diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 2ffb9f7..fbfafb9 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -108,6 +108,7 @@ jobs:
dotnet pack --no-build --configuration Release --include-symbols --output nupkgs -p:PackageVersion=${{ steps.gitversion.outputs.semVer }} Obvs.Serialization.ProtoBuf
dotnet pack --no-build --configuration Release --include-symbols --output nupkgs -p:PackageVersion=${{ steps.gitversion.outputs.semVer }} Obvs.Serialization.Xml
dotnet pack --no-build --configuration Release --include-symbols --output nupkgs -p:PackageVersion=${{ steps.gitversion.outputs.semVer }} Obvs.Serialization.Yaml
+ dotnet pack --no-build --configuration Release --include-symbols --output nupkgs -p:PackageVersion=${{ steps.gitversion.outputs.semVer }} Obvs.Testing
- name: NuGet Login
if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/'))
id: nuget
diff --git a/Directory.Packages.props b/Directory.Packages.props
index f9b3268..1ce4107 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -9,8 +9,10 @@
+
+
@@ -27,7 +29,7 @@
-
+
diff --git a/Obvs.Testing.Tests/Messages.cs b/Obvs.Testing.Tests/Messages.cs
new file mode 100644
index 0000000..bed88ff
--- /dev/null
+++ b/Obvs.Testing.Tests/Messages.cs
@@ -0,0 +1,26 @@
+using Obvs.Types;
+
+namespace Obvs.Testing.Tests
+{
+ public interface IOrderMessage : IMessage { }
+
+ public interface IShippingMessage : IMessage { }
+
+ public record PlaceOrder(string OrderId) : IOrderMessage, ICommand;
+
+ public record OrderPlaced(string OrderId) : IOrderMessage, IEvent;
+
+ public record GetQuote(string Sku) : IOrderMessage, IRequest
+ {
+ public string RequestId { get; set; }
+ public string RequesterId { get; set; }
+ }
+
+ public record Quote(string Sku, decimal Price) : IOrderMessage, IResponse
+ {
+ public string RequestId { get; set; }
+ public string RequesterId { get; set; }
+ }
+
+ public record ShipOrder(string OrderId) : IShippingMessage, ICommand;
+}
diff --git a/Obvs.Testing.Tests/Obvs.Testing.Tests.csproj b/Obvs.Testing.Tests/Obvs.Testing.Tests.csproj
new file mode 100644
index 0000000..f1483b0
--- /dev/null
+++ b/Obvs.Testing.Tests/Obvs.Testing.Tests.csproj
@@ -0,0 +1,20 @@
+
+
+ Exe
+ net10.0
+ false
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
diff --git a/Obvs.Testing.Tests/TestInMemoryTransport.cs b/Obvs.Testing.Tests/TestInMemoryTransport.cs
new file mode 100644
index 0000000..2d5aa6d
--- /dev/null
+++ b/Obvs.Testing.Tests/TestInMemoryTransport.cs
@@ -0,0 +1,154 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Reactive.Threading.Tasks;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Reactive.Testing;
+using Obvs.Hosting;
+using Obvs.Types;
+using Xunit;
+
+namespace Obvs.Testing.Tests
+{
+ // Exercises the transport on its own, through AddObvs and the bus, without the harness
+ public class TestInMemoryTransport
+ {
+ private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(5);
+
+ [Fact]
+ public async Task ShouldDeliverCommandsFromClientToServer()
+ {
+ using var host = await StartHostAsync(obvs => obvs.AddService("orders", service => service.UseInMemory()));
+ var bus = host.Services.GetRequiredService();
+
+ var received = bus.Commands.OfType().FirstAsync().ToTask();
+ await bus.SendAsync(new PlaceOrder("o-1"));
+
+ Assert.Equal("o-1", (await received.WaitAsync(Timeout, TestContext.Current.CancellationToken)).OrderId);
+ }
+
+ [Fact]
+ public async Task ShouldDeliverEventsFromServerToClient()
+ {
+ using var host = await StartHostAsync(obvs => obvs.AddService("orders", service => service.UseInMemory()));
+ var bus = host.Services.GetRequiredService();
+
+ var received = bus.Events.OfType().FirstAsync().ToTask();
+ await bus.PublishAsync(new OrderPlaced("o-1"));
+
+ Assert.Equal("o-1", (await received.WaitAsync(Timeout, TestContext.Current.CancellationToken)).OrderId);
+ }
+
+ [Fact]
+ public async Task ShouldReturnResponsesToTheRequester()
+ {
+ using var host = await StartHostAsync(obvs => obvs.AddService("orders", service => service.UseInMemory()));
+ var bus = host.Services.GetRequiredService();
+ using var server = bus.Requests.OfType()
+ .Subscribe(request => _ = bus.ReplyAsync(request, new Quote(request.Sku, 9.99m)));
+
+ var quote = await bus.GetResponses(new GetQuote("sku-1")).OfType().FirstAsync().ToTask()
+ .WaitAsync(Timeout, TestContext.Current.CancellationToken);
+
+ Assert.Equal(new Quote("sku-1", 9.99m), quote with { RequestId = null, RequesterId = null });
+ }
+
+ [Fact]
+ public async Task ShouldOnlyDeliverMessagesToTheirOwnService()
+ {
+ // The bus merges every service's commands, so a command delivered to both services would arrive twice.
+ // The test scheduler makes delivery deterministic, so "exactly once" needs no waiting.
+ var scheduler = new TestScheduler();
+ var broker = new InMemoryBroker(scheduler);
+ using var host = await StartHostAsync(obvs => obvs
+ .AddService("orders", service => service.UseInMemory(broker))
+ .AddService("shipping", service => service.UseInMemory(broker)));
+ var bus = host.Services.GetRequiredService();
+
+ var received = new List();
+ using var subscription = bus.Commands.Subscribe(received.Add);
+ await bus.SendAsync(new ShipOrder("o-1"));
+ scheduler.Start();
+
+ Assert.Equal(new ICommand[] { new ShipOrder("o-1") }, received);
+ }
+
+ [Fact]
+ public async Task ShouldDeliverMessagesInTheOrderTheyWereSent()
+ {
+ using var host = await StartHostAsync(obvs => obvs.AddService("orders", service => service.UseInMemory()));
+ var bus = host.Services.GetRequiredService();
+ var ids = Enumerable.Range(1, 100).Select(i => $"o-{i}").ToArray();
+
+ var received = bus.Commands.OfType().Take(ids.Length).Select(command => command.OrderId).ToArray().ToTask();
+ foreach (var id in ids)
+ {
+ await bus.SendAsync(new PlaceOrder(id));
+ }
+
+ Assert.Equal(ids, await received.WaitAsync(Timeout, TestContext.Current.CancellationToken));
+ }
+
+ [Fact]
+ public async Task ShouldGiveEachContainerItsOwnBroker()
+ {
+ using var hostA = await StartHostAsync(obvs => obvs.AddService("orders", service => service.UseInMemory()));
+ using var hostB = await StartHostAsync(obvs => obvs.AddService("orders", service => service.UseInMemory()));
+ var busA = hostA.Services.GetRequiredService();
+ var busB = hostB.Services.GetRequiredService();
+
+ Assert.NotSame(hostA.Services.GetRequiredService(), hostB.Services.GetRequiredService());
+
+ var receivedByB = busB.Commands.OfType().FirstAsync().ToTask();
+ var receivedByA = busA.Commands.OfType().FirstAsync().ToTask();
+ await busA.SendAsync(new PlaceOrder("o-1"));
+
+ await receivedByA.WaitAsync(Timeout, TestContext.Current.CancellationToken);
+ Assert.False(receivedByB.IsCompleted);
+ }
+
+ [Fact]
+ public async Task ShouldConnectHostsThatShareABroker()
+ {
+ var broker = new InMemoryBroker();
+ using var clientHost = await StartHostAsync(obvs => obvs.AddService("orders", service => service.UseInMemory(broker).AsClient()));
+ using var serverHost = await StartHostAsync(obvs => obvs.AddService("orders", service => service.UseInMemory(broker).AsServer()));
+
+ var received = serverHost.Services.GetRequiredService().Commands.OfType().FirstAsync().ToTask();
+ await clientHost.Services.GetRequiredService().SendAsync(new PlaceOrder("o-1"));
+
+ Assert.Equal("o-1", (await received.WaitAsync(Timeout, TestContext.Current.CancellationToken)).OrderId);
+ }
+
+ [Fact]
+ public async Task ShouldDeliverOnTheBrokersScheduler()
+ {
+ var scheduler = new TestScheduler();
+ var broker = new InMemoryBroker(scheduler);
+ using var host = await StartHostAsync(obvs => obvs.AddService("orders", service => service.UseInMemory(broker)));
+ var bus = host.Services.GetRequiredService();
+
+ PlaceOrder received = null;
+ using var subscription = bus.Commands.OfType().Subscribe(command => received = command);
+ await bus.SendAsync(new PlaceOrder("o-1"));
+
+ Assert.Null(received);
+
+ scheduler.Start();
+
+ Assert.Equal("o-1", received?.OrderId);
+ }
+
+ private static async Task StartHostAsync(Action configure)
+ {
+ var builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { DisableDefaults = true });
+ builder.Services.AddObvs(configure);
+ var host = builder.Build();
+ await host.StartAsync(TestContext.Current.CancellationToken);
+ return host;
+ }
+ }
+}
diff --git a/Obvs.Testing.Tests/TestObvsTestHarness.cs b/Obvs.Testing.Tests/TestObvsTestHarness.cs
new file mode 100644
index 0000000..f21746d
--- /dev/null
+++ b/Obvs.Testing.Tests/TestObvsTestHarness.cs
@@ -0,0 +1,153 @@
+using System;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Time.Testing;
+using Obvs.Hosting;
+using Xunit;
+
+namespace Obvs.Testing.Tests
+{
+ public class TestObvsTestHarness
+ {
+ [Fact]
+ public async Task ShouldReturnAMessagePublishedAfterWaitingStarts()
+ {
+ await using var harness = await StartOrdersHarnessAsync();
+
+ var waiting = harness.Published();
+ await harness.Bus.PublishAsync(new OrderPlaced("o-1"));
+
+ Assert.Equal("o-1", (await waiting).OrderId);
+ }
+
+ [Fact]
+ public async Task ShouldReturnAMessagePublishedBeforeWaitingStarts()
+ {
+ await using var harness = await StartOrdersHarnessAsync();
+ await harness.Bus.PublishAsync(new OrderPlaced("o-1"));
+
+ Assert.Equal("o-1", (await harness.Published()).OrderId);
+ }
+
+ [Fact]
+ public async Task ShouldReturnOnlyAMessageMatchingThePredicate()
+ {
+ await using var harness = await StartOrdersHarnessAsync();
+ await harness.Bus.PublishAsync(new OrderPlaced("o-1"));
+ await harness.Bus.PublishAsync(new OrderPlaced("o-2"));
+
+ Assert.Equal("o-2", (await harness.Published(placed => placed.OrderId == "o-2")).OrderId);
+ }
+
+ [Fact]
+ public async Task ShouldTestAServiceEndToEnd()
+ {
+ await using var harness = await StartOrdersHarnessAsync();
+ using var service = harness.Bus.Commands.OfType()
+ .Subscribe(command => _ = harness.Bus.PublishAsync(new OrderPlaced(command.OrderId)));
+
+ await harness.Client.SendAsync(new PlaceOrder("o-1"));
+
+ await harness.Published(placed => placed.OrderId == "o-1");
+ }
+
+ [Fact]
+ public async Task ShouldSeeCommandsSentByAClientOnlyApp()
+ {
+ await using var harness = await ObvsTestHarness.StartAsync(services => services.AddObvs(obvs => obvs
+ .AddService("orders", service => service.UseInMemory().AsClient())),
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ await harness.Client.SendAsync(new PlaceOrder("o-1"));
+
+ Assert.Equal("o-1", (await harness.Sent()).OrderId);
+ }
+
+ [Fact]
+ public async Task ShouldSeeRequestsAndReplies()
+ {
+ await using var harness = await StartOrdersHarnessAsync();
+ using var server = harness.Bus.Requests.OfType()
+ .Subscribe(request => _ = harness.Bus.ReplyAsync(request, new Quote(request.Sku, 9.99m)));
+
+ using var requester = harness.Client.GetResponses(new GetQuote("sku-1")).Subscribe(_ => { });
+
+ Assert.Equal("sku-1", (await harness.Requested()).Sku);
+ Assert.Equal(9.99m, (await harness.Replied(quote => quote.Sku == "sku-1")).Price);
+ }
+
+ [Fact]
+ public async Task ShouldRecordEveryMessageInOrder()
+ {
+ await using var harness = await StartOrdersHarnessAsync();
+ await harness.Client.SendAsync(new PlaceOrder("o-1"));
+ await harness.Bus.PublishAsync(new OrderPlaced("o-1"));
+ await harness.Published();
+
+ Assert.Equal(new object[] { new PlaceOrder("o-1"), new OrderPlaced("o-1") }, harness.Messages);
+ }
+
+ [Fact]
+ public async Task ShouldListTheMessagesSeenWhenWaitingTimesOut()
+ {
+ await using var harness = await StartOrdersHarnessAsync();
+ await harness.Bus.PublishAsync(new OrderPlaced("o-1"));
+ await harness.Published();
+
+ var exception = await Assert.ThrowsAsync(
+ () => harness.Published(placed => placed.OrderId == "o-2", TimeSpan.FromMilliseconds(100)));
+
+ Assert.Contains("OrderPlaced", exception.Message);
+ Assert.Contains("o-1", exception.Message);
+ }
+
+ [Fact]
+ public async Task ShouldUseTheDefaultTimeoutFromTheOptions()
+ {
+ await using var harness = await ObvsTestHarness.StartAsync(
+ services => services.AddObvs(obvs => obvs.AddService("orders", service => service.UseInMemory())),
+ options => options.DefaultTimeout = TimeSpan.FromMilliseconds(100),
+ TestContext.Current.CancellationToken);
+
+ await Assert.ThrowsAsync(() => harness.Published())
+ .WaitAsync(TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken);
+ }
+
+ [Fact]
+ public async Task ShouldTimeOutOnTheTimeProvider()
+ {
+ var time = new FakeTimeProvider();
+ await using var harness = await ObvsTestHarness.StartAsync(
+ services => services.AddObvs(obvs => obvs.AddService("orders", service => service.UseInMemory())),
+ options => options.TimeProvider = time,
+ TestContext.Current.CancellationToken);
+
+ var waiting = harness.Published(timeout: TimeSpan.FromMinutes(5));
+ time.Advance(TimeSpan.FromMinutes(6));
+
+ // Completes because virtual time passed, long before five real minutes
+ await Assert.ThrowsAsync(() => waiting)
+ .WaitAsync(TimeSpan.FromSeconds(2), TestContext.Current.CancellationToken);
+ }
+
+ [Fact]
+ public async Task ShouldStopTheHostWhenDisposed()
+ {
+ var harness = await StartOrdersHarnessAsync();
+ var lifetime = harness.Services.GetRequiredService();
+
+ await harness.DisposeAsync();
+
+ Assert.True(lifetime.ApplicationStopped.IsCancellationRequested);
+ }
+
+ private static Task StartOrdersHarnessAsync()
+ {
+ return ObvsTestHarness.StartAsync(services => services.AddObvs(obvs => obvs
+ .AddService("orders", service => service.UseInMemory())),
+ cancellationToken: TestContext.Current.CancellationToken);
+ }
+ }
+}
diff --git a/Obvs.Testing/InMemoryBroker.cs b/Obvs.Testing/InMemoryBroker.cs
new file mode 100644
index 0000000..5e9cfe2
--- /dev/null
+++ b/Obvs.Testing/InMemoryBroker.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Concurrent;
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using Obvs.Types;
+
+namespace Obvs.Testing
+{
+ ///
+ /// A message broker that lives in memory. Services that call UseInMemory() share one broker
+ /// per container, so each host is isolated; pass the same broker to several hosts to connect them.
+ ///
+ public sealed class InMemoryBroker
+ {
+ private readonly IScheduler _scheduler;
+ private readonly ConcurrentDictionary> _destinations = new ConcurrentDictionary>();
+ private readonly ISubject _messages = Subject.Synchronize(new Subject());
+
+ /// Creates a broker that delivers messages on the task pool, as a real broker's client threads would.
+ public InMemoryBroker()
+ : this(null)
+ {
+ }
+
+ ///
+ /// Creates a broker that delivers messages on .
+ /// Pass a TestScheduler to control exactly when messages are delivered in a test.
+ ///
+ public InMemoryBroker(IScheduler scheduler)
+ {
+ _scheduler = scheduler ?? TaskPoolScheduler.Default;
+ }
+
+ /// Every message passing through the broker, in the order it was sent, before it's delivered.
+ public IObservable Messages => _messages.AsObservable();
+
+ internal void Send(string serviceName, string destination, IMessage message)
+ {
+ _messages.OnNext(new InMemoryMessage(serviceName, message));
+ GetDestination(destination).OnNext(message);
+ }
+
+ // Each subscriber gets its own ordered queue on the scheduler, like a consumer on a real broker
+ internal IObservable Receive(string destination)
+ {
+ return GetDestination(destination).ObserveOn(_scheduler);
+ }
+
+ private ISubject GetDestination(string destination)
+ {
+ return _destinations.GetOrAdd(destination, _ => Subject.Synchronize(new Subject()));
+ }
+ }
+
+ /// A message passing through an .
+ public sealed class InMemoryMessage
+ {
+ public InMemoryMessage(string serviceName, IMessage message)
+ {
+ ServiceName = serviceName;
+ Message = message;
+ }
+
+ /// The name of the service the message belongs to.
+ public string ServiceName { get; }
+
+ public IMessage Message { get; }
+ }
+}
diff --git a/Obvs.Testing/InMemoryEndpointProvider.cs b/Obvs.Testing/InMemoryEndpointProvider.cs
new file mode 100644
index 0000000..4ce7ad4
--- /dev/null
+++ b/Obvs.Testing/InMemoryEndpointProvider.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using Obvs.Configuration;
+using Obvs.Types;
+
+namespace Obvs.Testing
+{
+ // One destination per service and kind of message, as the broker-based transports name them
+ internal sealed class InMemoryEndpointProvider : IServiceEndpointProvider
+ {
+ private readonly string _serviceName;
+ private readonly Type _serviceMessageType;
+ private readonly InMemoryBroker _broker;
+
+ public InMemoryEndpointProvider(string serviceName, Type serviceMessageType, InMemoryBroker broker)
+ {
+ _serviceName = serviceName;
+ _serviceMessageType = serviceMessageType;
+ _broker = broker;
+ }
+
+ public IServiceEndpoint CreateEndpoint()
+ {
+ return new ServiceEndpoint(
+ Source("Requests"),
+ Source("Commands"),
+ Publisher("Events"),
+ Publisher("Responses"),
+ _serviceMessageType);
+ }
+
+ public IServiceEndpointClient CreateEndpointClient()
+ {
+ return new ServiceEndpointClient(
+ Source("Events"),
+ Source("Responses"),
+ Publisher("Requests"),
+ Publisher("Commands"),
+ _serviceMessageType);
+ }
+
+ private IMessageSource Source(string kind) where TMessage : class, IMessage
+ {
+ return new InMemoryMessageSource(_broker, Destination(kind));
+ }
+
+ private IMessagePublisher Publisher(string kind) where TMessage : class, IMessage
+ {
+ return new InMemoryMessagePublisher(_broker, _serviceName, Destination(kind));
+ }
+
+ private string Destination(string kind)
+ {
+ return $"{_serviceName}.{kind}";
+ }
+
+ private sealed class InMemoryMessageSource : IMessageSource where TMessage : class, IMessage
+ {
+ private readonly InMemoryBroker _broker;
+ private readonly string _destination;
+
+ public InMemoryMessageSource(InMemoryBroker broker, string destination)
+ {
+ _broker = broker;
+ _destination = destination;
+ }
+
+ public IObservable Messages => _broker.Receive(_destination).OfType();
+
+ public void Dispose()
+ {
+ }
+ }
+
+ private sealed class InMemoryMessagePublisher : IMessagePublisher where TMessage : class, IMessage
+ {
+ private readonly InMemoryBroker _broker;
+ private readonly string _serviceName;
+ private readonly string _destination;
+
+ public InMemoryMessagePublisher(InMemoryBroker broker, string serviceName, string destination)
+ {
+ _broker = broker;
+ _serviceName = serviceName;
+ _destination = destination;
+ }
+
+ public Task PublishAsync(TMessage message)
+ {
+ _broker.Send(_serviceName, _destination, message);
+ return Task.CompletedTask;
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+ }
+}
diff --git a/Obvs.Testing/InMemoryTransportExtensions.cs b/Obvs.Testing/InMemoryTransportExtensions.cs
new file mode 100644
index 0000000..c47ac70
--- /dev/null
+++ b/Obvs.Testing/InMemoryTransportExtensions.cs
@@ -0,0 +1,52 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Obvs.Testing;
+
+// In the builder's namespace, so UseInMemory() is found without an extra using
+namespace Obvs.Hosting
+{
+ public static class InMemoryTransportExtensions
+ {
+ ///
+ /// Carries this service's messages in memory, on a broker shared by everything in the same container.
+ /// Delivery behaves like a real transport: per-service destinations, request/response correlation,
+ /// and asynchronous, in-order delivery. Messages are passed by reference, not serialized.
+ ///
+ public static ObvsServiceBuilder UseInMemory(this ObvsServiceBuilder service)
+ {
+ if (service == null)
+ {
+ throw new ArgumentNullException(nameof(service));
+ }
+
+ // TryAdd, so a broker registered first (as ObvsTestHarness does) is used instead
+ service.Services.TryAddSingleton(_ => new InMemoryBroker());
+ var name = service.Name;
+ var serviceMessageType = service.ServiceMessageType;
+ return service.UseEndpointProvider(serviceProvider =>
+ new InMemoryEndpointProvider(name, serviceMessageType, serviceProvider.GetRequiredService()));
+ }
+
+ ///
+ /// Carries this service's messages on . Use one broker in several hosts
+ /// to test services that run in separate processes in production.
+ ///
+ public static ObvsServiceBuilder UseInMemory(this ObvsServiceBuilder service, InMemoryBroker broker)
+ {
+ if (service == null)
+ {
+ throw new ArgumentNullException(nameof(service));
+ }
+ if (broker == null)
+ {
+ throw new ArgumentNullException(nameof(broker));
+ }
+
+ service.Services.TryAddSingleton(broker);
+ var name = service.Name;
+ var serviceMessageType = service.ServiceMessageType;
+ return service.UseEndpointProvider(_ => new InMemoryEndpointProvider(name, serviceMessageType, broker));
+ }
+ }
+}
diff --git a/Obvs.Testing/Obvs.Testing.csproj b/Obvs.Testing/Obvs.Testing.csproj
new file mode 100644
index 0000000..319110b
--- /dev/null
+++ b/Obvs.Testing/Obvs.Testing.csproj
@@ -0,0 +1,21 @@
+
+
+ net472;netstandard2.0;net10.0
+ Christopher Read
+ Copyright © Christopher Read 2026
+ 1.0.0
+ Christopher Read
+ http://christopherread.github.io/Obvs
+ obvs testing test harness in-memory messaging servicebus
+ In-memory transport and test harness for Obvs: test services end to end without a broker or fixed delays.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Obvs.Testing/ObvsTestHarness.cs b/Obvs.Testing/ObvsTestHarness.cs
new file mode 100644
index 0000000..aa6aaaf
--- /dev/null
+++ b/Obvs.Testing/ObvsTestHarness.cs
@@ -0,0 +1,204 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Reactive.Threading.Tasks;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Obvs.Types;
+
+namespace Obvs.Testing
+{
+ ///
+ /// Runs a host with an in-memory broker and waits for messages without fixed delays:
+ /// await harness.Published<OrderPlaced>(e => e.OrderId == "o-1") returns as soon as a
+ /// matching event passes through the broker, including one that already has.
+ ///
+ public sealed class ObvsTestHarness : IAsyncDisposable
+ {
+ private readonly IHost _host;
+ private readonly ObvsTestHarnessOptions _options;
+ private readonly ReplaySubject _seen = new ReplaySubject();
+ private readonly List _messages = new List();
+ private readonly IDisposable _recording;
+
+ private ObvsTestHarness(IHost host, InMemoryBroker broker, ObvsTestHarnessOptions options)
+ {
+ _host = host;
+ _options = options;
+ Broker = broker;
+ _recording = broker.Messages.Subscribe(Record);
+ }
+
+ ///
+ /// Builds and starts a host. Configure the bus with services.AddObvs(...) and
+ /// UseInMemory() for each service; they all share the harness's broker.
+ ///
+ public static async Task StartAsync(
+ Action configureServices,
+ Action configureOptions = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (configureServices == null)
+ {
+ throw new ArgumentNullException(nameof(configureServices));
+ }
+
+ var options = new ObvsTestHarnessOptions();
+ configureOptions?.Invoke(options);
+
+ var broker = new InMemoryBroker(options.Scheduler);
+ var builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { DisableDefaults = true });
+ // Registered before the caller's services, so UseInMemory() picks up this broker
+ builder.Services.AddSingleton(broker);
+ configureServices(builder.Services);
+
+ var harness = new ObvsTestHarness(builder.Build(), broker, options);
+ try
+ {
+ await harness._host.StartAsync(cancellationToken).ConfigureAwait(false);
+ }
+ catch
+ {
+ await harness.DisposeAsync().ConfigureAwait(false);
+ throw;
+ }
+ return harness;
+ }
+
+ /// The host's services.
+ public IServiceProvider Services => _host.Services;
+
+ /// The bus, if the services configured one with AddObvs.
+ public IServiceBus Bus => Services.GetService();
+
+ /// The bus, as a client.
+ public IServiceBusClient Client => Services.GetService();
+
+ /// The broker carrying every service's messages.
+ public InMemoryBroker Broker { get; }
+
+ /// Every message that has passed through the broker since the harness started, in order.
+ public IReadOnlyList Messages
+ {
+ get
+ {
+ lock (_messages)
+ {
+ return _messages.ToArray();
+ }
+ }
+ }
+
+ /// Waits for a command to be sent, or returns one already sent. Throws if none arrives in time.
+ public Task Sent(Func predicate = null, TimeSpan? timeout = null)
+ where TCommand : class, ICommand
+ {
+ return WaitFor(predicate, timeout, "sent");
+ }
+
+ /// Waits for an event to be published, or returns one already published. Throws if none arrives in time.
+ public Task Published(Func predicate = null, TimeSpan? timeout = null)
+ where TEvent : class, IEvent
+ {
+ return WaitFor(predicate, timeout, "published");
+ }
+
+ /// Waits for a request to be sent, or returns one already sent. Throws if none arrives in time.
+ public Task Requested(Func predicate = null, TimeSpan? timeout = null)
+ where TRequest : class, IRequest
+ {
+ return WaitFor(predicate, timeout, "requested");
+ }
+
+ /// Waits for a response to be sent, or returns one already sent. Throws if none arrives in time.
+ public Task Replied(Func predicate = null, TimeSpan? timeout = null)
+ where TResponse : class, IResponse
+ {
+ return WaitFor(predicate, timeout, "replied");
+ }
+
+ /// Stops the host and disposes its services, including the bus.
+ public async ValueTask DisposeAsync()
+ {
+ try
+ {
+ await _host.StopAsync().ConfigureAwait(false);
+ }
+ finally
+ {
+ _recording.Dispose();
+ if (_host is IAsyncDisposable asyncDisposable)
+ {
+ await asyncDisposable.DisposeAsync().ConfigureAwait(false);
+ }
+ else
+ {
+ _host.Dispose();
+ }
+ }
+ }
+
+ private void Record(InMemoryMessage message)
+ {
+ lock (_messages)
+ {
+ _messages.Add(message.Message);
+ }
+ _seen.OnNext(message.Message);
+ }
+
+ private async Task WaitFor(Func predicate, TimeSpan? timeout, string verb)
+ where TMessage : class
+ {
+ var timeLimit = timeout ?? _options.DefaultTimeout;
+ using (var cancellation = new CancellationTokenSource())
+ {
+ // Both started before the first await, so the timeout runs from when the caller asked
+ var match = _seen.OfType().Where(predicate ?? (_ => true)).FirstAsync().ToTask(cancellation.Token);
+ var expiry = Delay(timeLimit, cancellation.Token);
+
+ var first = await Task.WhenAny(match, expiry).ConfigureAwait(false);
+ cancellation.Cancel();
+ if (first == match)
+ {
+ return await match.ConfigureAwait(false);
+ }
+ }
+
+ throw new ObvsTestTimeoutException(DescribeTimeout(typeof(TMessage), verb, timeLimit));
+ }
+
+ private Task Delay(TimeSpan delay, CancellationToken cancellationToken)
+ {
+#if NET8_0_OR_GREATER
+ return Task.Delay(delay, _options.TimeProvider, cancellationToken);
+#else
+ return _options.TimeProvider.Delay(delay, cancellationToken);
+#endif
+ }
+
+ private string DescribeTimeout(Type expectedType, string verb, TimeSpan timeLimit)
+ {
+ var messages = Messages;
+ var description = $"No matching {expectedType.Name} was {verb} within {timeLimit}.";
+ if (messages.Count == 0)
+ {
+ return description + " No messages passed through the broker.";
+ }
+
+ return description + $" Messages that passed through the broker ({messages.Count}):" + Environment.NewLine +
+ string.Join(Environment.NewLine, messages.Select(message => "- " + Describe(message)));
+ }
+
+ // Records print their values; anything else falls back to its type name
+ private static string Describe(IMessage message)
+ {
+ var text = message.ToString();
+ return text == message.GetType().ToString() ? message.GetType().Name : text;
+ }
+ }
+}
diff --git a/Obvs.Testing/ObvsTestHarnessOptions.cs b/Obvs.Testing/ObvsTestHarnessOptions.cs
new file mode 100644
index 0000000..d7cda1b
--- /dev/null
+++ b/Obvs.Testing/ObvsTestHarnessOptions.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Reactive.Concurrency;
+
+namespace Obvs.Testing
+{
+ public sealed class ObvsTestHarnessOptions
+ {
+ /// How long Sent, Published, Requested and Replied wait when no timeout is passed. Five seconds by default.
+ public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(5);
+
+ ///
+ /// The clock timeouts are measured on. Pass a FakeTimeProvider to test timeouts by advancing
+ /// time instead of waiting.
+ ///
+ public TimeProvider TimeProvider { get; set; } = TimeProvider.System;
+
+ ///
+ /// Where the in-memory broker delivers messages. The task pool by default; pass a TestScheduler
+ /// to control delivery and virtual time in the Rx streams.
+ ///
+ public IScheduler Scheduler { get; set; }
+ }
+}
diff --git a/Obvs.Testing/ObvsTestTimeoutException.cs b/Obvs.Testing/ObvsTestTimeoutException.cs
new file mode 100644
index 0000000..2be8e7b
--- /dev/null
+++ b/Obvs.Testing/ObvsTestTimeoutException.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace Obvs.Testing
+{
+ ///
+ /// Thrown when an expected message didn't arrive in time. The message lists what did pass through
+ /// the broker, which usually shows what went wrong.
+ ///
+ public sealed class ObvsTestTimeoutException : TimeoutException
+ {
+ public ObvsTestTimeoutException(string message)
+ : base(message)
+ {
+ }
+ }
+}
diff --git a/Obvs.sln b/Obvs.sln
index ea858be..31f4678 100644
--- a/Obvs.sln
+++ b/Obvs.sln
@@ -69,132 +69,404 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Obvs.RabbitMQ", "Obvs.Rabbi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Obvs.RabbitMQ.Tests", "Obvs.RabbitMQ.Tests\Obvs.RabbitMQ.Tests.csproj", "{3D3152EF-C549-4B32-A4BB-6C2ED8ADDC38}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Obvs.Testing", "Obvs.Testing\Obvs.Testing.csproj", "{831D6502-4AAD-410A-A190-6A6DAEEBBE7E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Obvs.Testing.Tests", "Obvs.Testing.Tests\Obvs.Testing.Tests.csproj", "{044D8336-95A6-4A61-B229-8AFFEC259922}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{325264EE-18AA-4DF4-9306-3FE7A1F49A98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{325264EE-18AA-4DF4-9306-3FE7A1F49A98}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {325264EE-18AA-4DF4-9306-3FE7A1F49A98}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {325264EE-18AA-4DF4-9306-3FE7A1F49A98}.Debug|x64.Build.0 = Debug|Any CPU
+ {325264EE-18AA-4DF4-9306-3FE7A1F49A98}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {325264EE-18AA-4DF4-9306-3FE7A1F49A98}.Debug|x86.Build.0 = Debug|Any CPU
{325264EE-18AA-4DF4-9306-3FE7A1F49A98}.Release|Any CPU.ActiveCfg = Release|Any CPU
{325264EE-18AA-4DF4-9306-3FE7A1F49A98}.Release|Any CPU.Build.0 = Release|Any CPU
+ {325264EE-18AA-4DF4-9306-3FE7A1F49A98}.Release|x64.ActiveCfg = Release|Any CPU
+ {325264EE-18AA-4DF4-9306-3FE7A1F49A98}.Release|x64.Build.0 = Release|Any CPU
+ {325264EE-18AA-4DF4-9306-3FE7A1F49A98}.Release|x86.ActiveCfg = Release|Any CPU
+ {325264EE-18AA-4DF4-9306-3FE7A1F49A98}.Release|x86.Build.0 = Release|Any CPU
{A50FE991-84FA-4E48-BEF7-B0DB8F0F09D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A50FE991-84FA-4E48-BEF7-B0DB8F0F09D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A50FE991-84FA-4E48-BEF7-B0DB8F0F09D8}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A50FE991-84FA-4E48-BEF7-B0DB8F0F09D8}.Debug|x64.Build.0 = Debug|Any CPU
+ {A50FE991-84FA-4E48-BEF7-B0DB8F0F09D8}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A50FE991-84FA-4E48-BEF7-B0DB8F0F09D8}.Debug|x86.Build.0 = Debug|Any CPU
{A50FE991-84FA-4E48-BEF7-B0DB8F0F09D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A50FE991-84FA-4E48-BEF7-B0DB8F0F09D8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A50FE991-84FA-4E48-BEF7-B0DB8F0F09D8}.Release|x64.ActiveCfg = Release|Any CPU
+ {A50FE991-84FA-4E48-BEF7-B0DB8F0F09D8}.Release|x64.Build.0 = Release|Any CPU
+ {A50FE991-84FA-4E48-BEF7-B0DB8F0F09D8}.Release|x86.ActiveCfg = Release|Any CPU
+ {A50FE991-84FA-4E48-BEF7-B0DB8F0F09D8}.Release|x86.Build.0 = Release|Any CPU
{ECDA9415-7F7B-4CBD-B279-CE34329CD88B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ECDA9415-7F7B-4CBD-B279-CE34329CD88B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ECDA9415-7F7B-4CBD-B279-CE34329CD88B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {ECDA9415-7F7B-4CBD-B279-CE34329CD88B}.Debug|x64.Build.0 = Debug|Any CPU
+ {ECDA9415-7F7B-4CBD-B279-CE34329CD88B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {ECDA9415-7F7B-4CBD-B279-CE34329CD88B}.Debug|x86.Build.0 = Debug|Any CPU
{ECDA9415-7F7B-4CBD-B279-CE34329CD88B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ECDA9415-7F7B-4CBD-B279-CE34329CD88B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ECDA9415-7F7B-4CBD-B279-CE34329CD88B}.Release|x64.ActiveCfg = Release|Any CPU
+ {ECDA9415-7F7B-4CBD-B279-CE34329CD88B}.Release|x64.Build.0 = Release|Any CPU
+ {ECDA9415-7F7B-4CBD-B279-CE34329CD88B}.Release|x86.ActiveCfg = Release|Any CPU
+ {ECDA9415-7F7B-4CBD-B279-CE34329CD88B}.Release|x86.Build.0 = Release|Any CPU
{687BF25D-52BB-40B1-8749-691E9B7981D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{687BF25D-52BB-40B1-8749-691E9B7981D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {687BF25D-52BB-40B1-8749-691E9B7981D9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {687BF25D-52BB-40B1-8749-691E9B7981D9}.Debug|x64.Build.0 = Debug|Any CPU
+ {687BF25D-52BB-40B1-8749-691E9B7981D9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {687BF25D-52BB-40B1-8749-691E9B7981D9}.Debug|x86.Build.0 = Debug|Any CPU
{687BF25D-52BB-40B1-8749-691E9B7981D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{687BF25D-52BB-40B1-8749-691E9B7981D9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {687BF25D-52BB-40B1-8749-691E9B7981D9}.Release|x64.ActiveCfg = Release|Any CPU
+ {687BF25D-52BB-40B1-8749-691E9B7981D9}.Release|x64.Build.0 = Release|Any CPU
+ {687BF25D-52BB-40B1-8749-691E9B7981D9}.Release|x86.ActiveCfg = Release|Any CPU
+ {687BF25D-52BB-40B1-8749-691E9B7981D9}.Release|x86.Build.0 = Release|Any CPU
{56BF20F0-B816-4448-B516-FCCD72F34359}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{56BF20F0-B816-4448-B516-FCCD72F34359}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {56BF20F0-B816-4448-B516-FCCD72F34359}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {56BF20F0-B816-4448-B516-FCCD72F34359}.Debug|x64.Build.0 = Debug|Any CPU
+ {56BF20F0-B816-4448-B516-FCCD72F34359}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {56BF20F0-B816-4448-B516-FCCD72F34359}.Debug|x86.Build.0 = Debug|Any CPU
{56BF20F0-B816-4448-B516-FCCD72F34359}.Release|Any CPU.ActiveCfg = Release|Any CPU
{56BF20F0-B816-4448-B516-FCCD72F34359}.Release|Any CPU.Build.0 = Release|Any CPU
+ {56BF20F0-B816-4448-B516-FCCD72F34359}.Release|x64.ActiveCfg = Release|Any CPU
+ {56BF20F0-B816-4448-B516-FCCD72F34359}.Release|x64.Build.0 = Release|Any CPU
+ {56BF20F0-B816-4448-B516-FCCD72F34359}.Release|x86.ActiveCfg = Release|Any CPU
+ {56BF20F0-B816-4448-B516-FCCD72F34359}.Release|x86.Build.0 = Release|Any CPU
{0980B30C-18EF-4CDB-A255-67A40FECC511}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0980B30C-18EF-4CDB-A255-67A40FECC511}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0980B30C-18EF-4CDB-A255-67A40FECC511}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0980B30C-18EF-4CDB-A255-67A40FECC511}.Debug|x64.Build.0 = Debug|Any CPU
+ {0980B30C-18EF-4CDB-A255-67A40FECC511}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0980B30C-18EF-4CDB-A255-67A40FECC511}.Debug|x86.Build.0 = Debug|Any CPU
{0980B30C-18EF-4CDB-A255-67A40FECC511}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0980B30C-18EF-4CDB-A255-67A40FECC511}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0980B30C-18EF-4CDB-A255-67A40FECC511}.Release|x64.ActiveCfg = Release|Any CPU
+ {0980B30C-18EF-4CDB-A255-67A40FECC511}.Release|x64.Build.0 = Release|Any CPU
+ {0980B30C-18EF-4CDB-A255-67A40FECC511}.Release|x86.ActiveCfg = Release|Any CPU
+ {0980B30C-18EF-4CDB-A255-67A40FECC511}.Release|x86.Build.0 = Release|Any CPU
{7B1C02EC-9D33-45B2-BE0E-23EE42F7148A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7B1C02EC-9D33-45B2-BE0E-23EE42F7148A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7B1C02EC-9D33-45B2-BE0E-23EE42F7148A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7B1C02EC-9D33-45B2-BE0E-23EE42F7148A}.Debug|x64.Build.0 = Debug|Any CPU
+ {7B1C02EC-9D33-45B2-BE0E-23EE42F7148A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7B1C02EC-9D33-45B2-BE0E-23EE42F7148A}.Debug|x86.Build.0 = Debug|Any CPU
{7B1C02EC-9D33-45B2-BE0E-23EE42F7148A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7B1C02EC-9D33-45B2-BE0E-23EE42F7148A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7B1C02EC-9D33-45B2-BE0E-23EE42F7148A}.Release|x64.ActiveCfg = Release|Any CPU
+ {7B1C02EC-9D33-45B2-BE0E-23EE42F7148A}.Release|x64.Build.0 = Release|Any CPU
+ {7B1C02EC-9D33-45B2-BE0E-23EE42F7148A}.Release|x86.ActiveCfg = Release|Any CPU
+ {7B1C02EC-9D33-45B2-BE0E-23EE42F7148A}.Release|x86.Build.0 = Release|Any CPU
{E1F0A6A1-1371-41A1-BFC4-85B0ED445914}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E1F0A6A1-1371-41A1-BFC4-85B0ED445914}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E1F0A6A1-1371-41A1-BFC4-85B0ED445914}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E1F0A6A1-1371-41A1-BFC4-85B0ED445914}.Debug|x64.Build.0 = Debug|Any CPU
+ {E1F0A6A1-1371-41A1-BFC4-85B0ED445914}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E1F0A6A1-1371-41A1-BFC4-85B0ED445914}.Debug|x86.Build.0 = Debug|Any CPU
{E1F0A6A1-1371-41A1-BFC4-85B0ED445914}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E1F0A6A1-1371-41A1-BFC4-85B0ED445914}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E1F0A6A1-1371-41A1-BFC4-85B0ED445914}.Release|x64.ActiveCfg = Release|Any CPU
+ {E1F0A6A1-1371-41A1-BFC4-85B0ED445914}.Release|x64.Build.0 = Release|Any CPU
+ {E1F0A6A1-1371-41A1-BFC4-85B0ED445914}.Release|x86.ActiveCfg = Release|Any CPU
+ {E1F0A6A1-1371-41A1-BFC4-85B0ED445914}.Release|x86.Build.0 = Release|Any CPU
{D692B23C-8693-4625-8625-CAE3D47992E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D692B23C-8693-4625-8625-CAE3D47992E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D692B23C-8693-4625-8625-CAE3D47992E1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D692B23C-8693-4625-8625-CAE3D47992E1}.Debug|x64.Build.0 = Debug|Any CPU
+ {D692B23C-8693-4625-8625-CAE3D47992E1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D692B23C-8693-4625-8625-CAE3D47992E1}.Debug|x86.Build.0 = Debug|Any CPU
{D692B23C-8693-4625-8625-CAE3D47992E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D692B23C-8693-4625-8625-CAE3D47992E1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D692B23C-8693-4625-8625-CAE3D47992E1}.Release|x64.ActiveCfg = Release|Any CPU
+ {D692B23C-8693-4625-8625-CAE3D47992E1}.Release|x64.Build.0 = Release|Any CPU
+ {D692B23C-8693-4625-8625-CAE3D47992E1}.Release|x86.ActiveCfg = Release|Any CPU
+ {D692B23C-8693-4625-8625-CAE3D47992E1}.Release|x86.Build.0 = Release|Any CPU
{339F6982-B03C-4063-A483-B99EBFD549C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{339F6982-B03C-4063-A483-B99EBFD549C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {339F6982-B03C-4063-A483-B99EBFD549C5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {339F6982-B03C-4063-A483-B99EBFD549C5}.Debug|x64.Build.0 = Debug|Any CPU
+ {339F6982-B03C-4063-A483-B99EBFD549C5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {339F6982-B03C-4063-A483-B99EBFD549C5}.Debug|x86.Build.0 = Debug|Any CPU
{339F6982-B03C-4063-A483-B99EBFD549C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{339F6982-B03C-4063-A483-B99EBFD549C5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {339F6982-B03C-4063-A483-B99EBFD549C5}.Release|x64.ActiveCfg = Release|Any CPU
+ {339F6982-B03C-4063-A483-B99EBFD549C5}.Release|x64.Build.0 = Release|Any CPU
+ {339F6982-B03C-4063-A483-B99EBFD549C5}.Release|x86.ActiveCfg = Release|Any CPU
+ {339F6982-B03C-4063-A483-B99EBFD549C5}.Release|x86.Build.0 = Release|Any CPU
{2B7F41CD-2B5E-42F3-9C6E-2F64725F7204}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2B7F41CD-2B5E-42F3-9C6E-2F64725F7204}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2B7F41CD-2B5E-42F3-9C6E-2F64725F7204}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2B7F41CD-2B5E-42F3-9C6E-2F64725F7204}.Debug|x64.Build.0 = Debug|Any CPU
+ {2B7F41CD-2B5E-42F3-9C6E-2F64725F7204}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2B7F41CD-2B5E-42F3-9C6E-2F64725F7204}.Debug|x86.Build.0 = Debug|Any CPU
{2B7F41CD-2B5E-42F3-9C6E-2F64725F7204}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B7F41CD-2B5E-42F3-9C6E-2F64725F7204}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2B7F41CD-2B5E-42F3-9C6E-2F64725F7204}.Release|x64.ActiveCfg = Release|Any CPU
+ {2B7F41CD-2B5E-42F3-9C6E-2F64725F7204}.Release|x64.Build.0 = Release|Any CPU
+ {2B7F41CD-2B5E-42F3-9C6E-2F64725F7204}.Release|x86.ActiveCfg = Release|Any CPU
+ {2B7F41CD-2B5E-42F3-9C6E-2F64725F7204}.Release|x86.Build.0 = Release|Any CPU
{A9E1514B-0C22-41DD-958E-97AAB47A2474}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A9E1514B-0C22-41DD-958E-97AAB47A2474}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A9E1514B-0C22-41DD-958E-97AAB47A2474}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A9E1514B-0C22-41DD-958E-97AAB47A2474}.Debug|x64.Build.0 = Debug|Any CPU
+ {A9E1514B-0C22-41DD-958E-97AAB47A2474}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A9E1514B-0C22-41DD-958E-97AAB47A2474}.Debug|x86.Build.0 = Debug|Any CPU
{A9E1514B-0C22-41DD-958E-97AAB47A2474}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A9E1514B-0C22-41DD-958E-97AAB47A2474}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A9E1514B-0C22-41DD-958E-97AAB47A2474}.Release|x64.ActiveCfg = Release|Any CPU
+ {A9E1514B-0C22-41DD-958E-97AAB47A2474}.Release|x64.Build.0 = Release|Any CPU
+ {A9E1514B-0C22-41DD-958E-97AAB47A2474}.Release|x86.ActiveCfg = Release|Any CPU
+ {A9E1514B-0C22-41DD-958E-97AAB47A2474}.Release|x86.Build.0 = Release|Any CPU
{0B6AE569-3215-47CB-987A-6F8CC99682AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0B6AE569-3215-47CB-987A-6F8CC99682AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0B6AE569-3215-47CB-987A-6F8CC99682AD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0B6AE569-3215-47CB-987A-6F8CC99682AD}.Debug|x64.Build.0 = Debug|Any CPU
+ {0B6AE569-3215-47CB-987A-6F8CC99682AD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0B6AE569-3215-47CB-987A-6F8CC99682AD}.Debug|x86.Build.0 = Debug|Any CPU
{0B6AE569-3215-47CB-987A-6F8CC99682AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0B6AE569-3215-47CB-987A-6F8CC99682AD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0B6AE569-3215-47CB-987A-6F8CC99682AD}.Release|x64.ActiveCfg = Release|Any CPU
+ {0B6AE569-3215-47CB-987A-6F8CC99682AD}.Release|x64.Build.0 = Release|Any CPU
+ {0B6AE569-3215-47CB-987A-6F8CC99682AD}.Release|x86.ActiveCfg = Release|Any CPU
+ {0B6AE569-3215-47CB-987A-6F8CC99682AD}.Release|x86.Build.0 = Release|Any CPU
{790E6F10-8729-4319-B48A-4DAC42C0CCC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{790E6F10-8729-4319-B48A-4DAC42C0CCC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {790E6F10-8729-4319-B48A-4DAC42C0CCC1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {790E6F10-8729-4319-B48A-4DAC42C0CCC1}.Debug|x64.Build.0 = Debug|Any CPU
+ {790E6F10-8729-4319-B48A-4DAC42C0CCC1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {790E6F10-8729-4319-B48A-4DAC42C0CCC1}.Debug|x86.Build.0 = Debug|Any CPU
{790E6F10-8729-4319-B48A-4DAC42C0CCC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{790E6F10-8729-4319-B48A-4DAC42C0CCC1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {790E6F10-8729-4319-B48A-4DAC42C0CCC1}.Release|x64.ActiveCfg = Release|Any CPU
+ {790E6F10-8729-4319-B48A-4DAC42C0CCC1}.Release|x64.Build.0 = Release|Any CPU
+ {790E6F10-8729-4319-B48A-4DAC42C0CCC1}.Release|x86.ActiveCfg = Release|Any CPU
+ {790E6F10-8729-4319-B48A-4DAC42C0CCC1}.Release|x86.Build.0 = Release|Any CPU
{E8B360C0-23EE-421A-BAB5-394407674E99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E8B360C0-23EE-421A-BAB5-394407674E99}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E8B360C0-23EE-421A-BAB5-394407674E99}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E8B360C0-23EE-421A-BAB5-394407674E99}.Debug|x64.Build.0 = Debug|Any CPU
+ {E8B360C0-23EE-421A-BAB5-394407674E99}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E8B360C0-23EE-421A-BAB5-394407674E99}.Debug|x86.Build.0 = Debug|Any CPU
{E8B360C0-23EE-421A-BAB5-394407674E99}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E8B360C0-23EE-421A-BAB5-394407674E99}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E8B360C0-23EE-421A-BAB5-394407674E99}.Release|x64.ActiveCfg = Release|Any CPU
+ {E8B360C0-23EE-421A-BAB5-394407674E99}.Release|x64.Build.0 = Release|Any CPU
+ {E8B360C0-23EE-421A-BAB5-394407674E99}.Release|x86.ActiveCfg = Release|Any CPU
+ {E8B360C0-23EE-421A-BAB5-394407674E99}.Release|x86.Build.0 = Release|Any CPU
{7B021AC2-7709-431A-9EF0-DD7319FF4431}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7B021AC2-7709-431A-9EF0-DD7319FF4431}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7B021AC2-7709-431A-9EF0-DD7319FF4431}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7B021AC2-7709-431A-9EF0-DD7319FF4431}.Debug|x64.Build.0 = Debug|Any CPU
+ {7B021AC2-7709-431A-9EF0-DD7319FF4431}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7B021AC2-7709-431A-9EF0-DD7319FF4431}.Debug|x86.Build.0 = Debug|Any CPU
{7B021AC2-7709-431A-9EF0-DD7319FF4431}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7B021AC2-7709-431A-9EF0-DD7319FF4431}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7B021AC2-7709-431A-9EF0-DD7319FF4431}.Release|x64.ActiveCfg = Release|Any CPU
+ {7B021AC2-7709-431A-9EF0-DD7319FF4431}.Release|x64.Build.0 = Release|Any CPU
+ {7B021AC2-7709-431A-9EF0-DD7319FF4431}.Release|x86.ActiveCfg = Release|Any CPU
+ {7B021AC2-7709-431A-9EF0-DD7319FF4431}.Release|x86.Build.0 = Release|Any CPU
{5C476CC2-6DD0-4C1B-A61D-37EF6CDFDA39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5C476CC2-6DD0-4C1B-A61D-37EF6CDFDA39}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5C476CC2-6DD0-4C1B-A61D-37EF6CDFDA39}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5C476CC2-6DD0-4C1B-A61D-37EF6CDFDA39}.Debug|x64.Build.0 = Debug|Any CPU
+ {5C476CC2-6DD0-4C1B-A61D-37EF6CDFDA39}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5C476CC2-6DD0-4C1B-A61D-37EF6CDFDA39}.Debug|x86.Build.0 = Debug|Any CPU
{5C476CC2-6DD0-4C1B-A61D-37EF6CDFDA39}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5C476CC2-6DD0-4C1B-A61D-37EF6CDFDA39}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5C476CC2-6DD0-4C1B-A61D-37EF6CDFDA39}.Release|x64.ActiveCfg = Release|Any CPU
+ {5C476CC2-6DD0-4C1B-A61D-37EF6CDFDA39}.Release|x64.Build.0 = Release|Any CPU
+ {5C476CC2-6DD0-4C1B-A61D-37EF6CDFDA39}.Release|x86.ActiveCfg = Release|Any CPU
+ {5C476CC2-6DD0-4C1B-A61D-37EF6CDFDA39}.Release|x86.Build.0 = Release|Any CPU
{38B7073C-6EA4-4699-870E-5252FDEF4742}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{38B7073C-6EA4-4699-870E-5252FDEF4742}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {38B7073C-6EA4-4699-870E-5252FDEF4742}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {38B7073C-6EA4-4699-870E-5252FDEF4742}.Debug|x64.Build.0 = Debug|Any CPU
+ {38B7073C-6EA4-4699-870E-5252FDEF4742}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {38B7073C-6EA4-4699-870E-5252FDEF4742}.Debug|x86.Build.0 = Debug|Any CPU
{38B7073C-6EA4-4699-870E-5252FDEF4742}.Release|Any CPU.ActiveCfg = Release|Any CPU
{38B7073C-6EA4-4699-870E-5252FDEF4742}.Release|Any CPU.Build.0 = Release|Any CPU
+ {38B7073C-6EA4-4699-870E-5252FDEF4742}.Release|x64.ActiveCfg = Release|Any CPU
+ {38B7073C-6EA4-4699-870E-5252FDEF4742}.Release|x64.Build.0 = Release|Any CPU
+ {38B7073C-6EA4-4699-870E-5252FDEF4742}.Release|x86.ActiveCfg = Release|Any CPU
+ {38B7073C-6EA4-4699-870E-5252FDEF4742}.Release|x86.Build.0 = Release|Any CPU
{9207A6C7-C972-4E4A-AA35-165F44000C70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9207A6C7-C972-4E4A-AA35-165F44000C70}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9207A6C7-C972-4E4A-AA35-165F44000C70}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9207A6C7-C972-4E4A-AA35-165F44000C70}.Debug|x64.Build.0 = Debug|Any CPU
+ {9207A6C7-C972-4E4A-AA35-165F44000C70}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9207A6C7-C972-4E4A-AA35-165F44000C70}.Debug|x86.Build.0 = Debug|Any CPU
{9207A6C7-C972-4E4A-AA35-165F44000C70}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9207A6C7-C972-4E4A-AA35-165F44000C70}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9207A6C7-C972-4E4A-AA35-165F44000C70}.Release|x64.ActiveCfg = Release|Any CPU
+ {9207A6C7-C972-4E4A-AA35-165F44000C70}.Release|x64.Build.0 = Release|Any CPU
+ {9207A6C7-C972-4E4A-AA35-165F44000C70}.Release|x86.ActiveCfg = Release|Any CPU
+ {9207A6C7-C972-4E4A-AA35-165F44000C70}.Release|x86.Build.0 = Release|Any CPU
{54B3FD7C-C459-4A85-8143-803B12A78F08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{54B3FD7C-C459-4A85-8143-803B12A78F08}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {54B3FD7C-C459-4A85-8143-803B12A78F08}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {54B3FD7C-C459-4A85-8143-803B12A78F08}.Debug|x64.Build.0 = Debug|Any CPU
+ {54B3FD7C-C459-4A85-8143-803B12A78F08}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {54B3FD7C-C459-4A85-8143-803B12A78F08}.Debug|x86.Build.0 = Debug|Any CPU
{54B3FD7C-C459-4A85-8143-803B12A78F08}.Release|Any CPU.ActiveCfg = Release|Any CPU
{54B3FD7C-C459-4A85-8143-803B12A78F08}.Release|Any CPU.Build.0 = Release|Any CPU
+ {54B3FD7C-C459-4A85-8143-803B12A78F08}.Release|x64.ActiveCfg = Release|Any CPU
+ {54B3FD7C-C459-4A85-8143-803B12A78F08}.Release|x64.Build.0 = Release|Any CPU
+ {54B3FD7C-C459-4A85-8143-803B12A78F08}.Release|x86.ActiveCfg = Release|Any CPU
+ {54B3FD7C-C459-4A85-8143-803B12A78F08}.Release|x86.Build.0 = Release|Any CPU
{2DC075C4-4B16-4E2D-B386-FA368B91B21C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2DC075C4-4B16-4E2D-B386-FA368B91B21C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2DC075C4-4B16-4E2D-B386-FA368B91B21C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2DC075C4-4B16-4E2D-B386-FA368B91B21C}.Debug|x64.Build.0 = Debug|Any CPU
+ {2DC075C4-4B16-4E2D-B386-FA368B91B21C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2DC075C4-4B16-4E2D-B386-FA368B91B21C}.Debug|x86.Build.0 = Debug|Any CPU
{2DC075C4-4B16-4E2D-B386-FA368B91B21C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2DC075C4-4B16-4E2D-B386-FA368B91B21C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2DC075C4-4B16-4E2D-B386-FA368B91B21C}.Release|x64.ActiveCfg = Release|Any CPU
+ {2DC075C4-4B16-4E2D-B386-FA368B91B21C}.Release|x64.Build.0 = Release|Any CPU
+ {2DC075C4-4B16-4E2D-B386-FA368B91B21C}.Release|x86.ActiveCfg = Release|Any CPU
+ {2DC075C4-4B16-4E2D-B386-FA368B91B21C}.Release|x86.Build.0 = Release|Any CPU
{CB105E02-748C-4335-A319-0D4AA87ABFAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CB105E02-748C-4335-A319-0D4AA87ABFAC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CB105E02-748C-4335-A319-0D4AA87ABFAC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CB105E02-748C-4335-A319-0D4AA87ABFAC}.Debug|x64.Build.0 = Debug|Any CPU
+ {CB105E02-748C-4335-A319-0D4AA87ABFAC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CB105E02-748C-4335-A319-0D4AA87ABFAC}.Debug|x86.Build.0 = Debug|Any CPU
{CB105E02-748C-4335-A319-0D4AA87ABFAC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CB105E02-748C-4335-A319-0D4AA87ABFAC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CB105E02-748C-4335-A319-0D4AA87ABFAC}.Release|x64.ActiveCfg = Release|Any CPU
+ {CB105E02-748C-4335-A319-0D4AA87ABFAC}.Release|x64.Build.0 = Release|Any CPU
+ {CB105E02-748C-4335-A319-0D4AA87ABFAC}.Release|x86.ActiveCfg = Release|Any CPU
+ {CB105E02-748C-4335-A319-0D4AA87ABFAC}.Release|x86.Build.0 = Release|Any CPU
{8A5CEA68-F55D-4229-93AB-64514F2DDB5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A5CEA68-F55D-4229-93AB-64514F2DDB5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8A5CEA68-F55D-4229-93AB-64514F2DDB5D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8A5CEA68-F55D-4229-93AB-64514F2DDB5D}.Debug|x64.Build.0 = Debug|Any CPU
+ {8A5CEA68-F55D-4229-93AB-64514F2DDB5D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8A5CEA68-F55D-4229-93AB-64514F2DDB5D}.Debug|x86.Build.0 = Debug|Any CPU
{8A5CEA68-F55D-4229-93AB-64514F2DDB5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A5CEA68-F55D-4229-93AB-64514F2DDB5D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8A5CEA68-F55D-4229-93AB-64514F2DDB5D}.Release|x64.ActiveCfg = Release|Any CPU
+ {8A5CEA68-F55D-4229-93AB-64514F2DDB5D}.Release|x64.Build.0 = Release|Any CPU
+ {8A5CEA68-F55D-4229-93AB-64514F2DDB5D}.Release|x86.ActiveCfg = Release|Any CPU
+ {8A5CEA68-F55D-4229-93AB-64514F2DDB5D}.Release|x86.Build.0 = Release|Any CPU
{91E800E6-D6B4-4D66-967D-43944549BEE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{91E800E6-D6B4-4D66-967D-43944549BEE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {91E800E6-D6B4-4D66-967D-43944549BEE9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {91E800E6-D6B4-4D66-967D-43944549BEE9}.Debug|x64.Build.0 = Debug|Any CPU
+ {91E800E6-D6B4-4D66-967D-43944549BEE9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {91E800E6-D6B4-4D66-967D-43944549BEE9}.Debug|x86.Build.0 = Debug|Any CPU
{91E800E6-D6B4-4D66-967D-43944549BEE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{91E800E6-D6B4-4D66-967D-43944549BEE9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {91E800E6-D6B4-4D66-967D-43944549BEE9}.Release|x64.ActiveCfg = Release|Any CPU
+ {91E800E6-D6B4-4D66-967D-43944549BEE9}.Release|x64.Build.0 = Release|Any CPU
+ {91E800E6-D6B4-4D66-967D-43944549BEE9}.Release|x86.ActiveCfg = Release|Any CPU
+ {91E800E6-D6B4-4D66-967D-43944549BEE9}.Release|x86.Build.0 = Release|Any CPU
{740FF1FC-2E27-4F77-8FC2-5E8B4FD8B084}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{740FF1FC-2E27-4F77-8FC2-5E8B4FD8B084}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {740FF1FC-2E27-4F77-8FC2-5E8B4FD8B084}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {740FF1FC-2E27-4F77-8FC2-5E8B4FD8B084}.Debug|x64.Build.0 = Debug|Any CPU
+ {740FF1FC-2E27-4F77-8FC2-5E8B4FD8B084}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {740FF1FC-2E27-4F77-8FC2-5E8B4FD8B084}.Debug|x86.Build.0 = Debug|Any CPU
{740FF1FC-2E27-4F77-8FC2-5E8B4FD8B084}.Release|Any CPU.ActiveCfg = Release|Any CPU
{740FF1FC-2E27-4F77-8FC2-5E8B4FD8B084}.Release|Any CPU.Build.0 = Release|Any CPU
+ {740FF1FC-2E27-4F77-8FC2-5E8B4FD8B084}.Release|x64.ActiveCfg = Release|Any CPU
+ {740FF1FC-2E27-4F77-8FC2-5E8B4FD8B084}.Release|x64.Build.0 = Release|Any CPU
+ {740FF1FC-2E27-4F77-8FC2-5E8B4FD8B084}.Release|x86.ActiveCfg = Release|Any CPU
+ {740FF1FC-2E27-4F77-8FC2-5E8B4FD8B084}.Release|x86.Build.0 = Release|Any CPU
{828B996B-7A49-4289-A766-B53971F51963}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{828B996B-7A49-4289-A766-B53971F51963}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {828B996B-7A49-4289-A766-B53971F51963}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {828B996B-7A49-4289-A766-B53971F51963}.Debug|x64.Build.0 = Debug|Any CPU
+ {828B996B-7A49-4289-A766-B53971F51963}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {828B996B-7A49-4289-A766-B53971F51963}.Debug|x86.Build.0 = Debug|Any CPU
{828B996B-7A49-4289-A766-B53971F51963}.Release|Any CPU.ActiveCfg = Release|Any CPU
{828B996B-7A49-4289-A766-B53971F51963}.Release|Any CPU.Build.0 = Release|Any CPU
+ {828B996B-7A49-4289-A766-B53971F51963}.Release|x64.ActiveCfg = Release|Any CPU
+ {828B996B-7A49-4289-A766-B53971F51963}.Release|x64.Build.0 = Release|Any CPU
+ {828B996B-7A49-4289-A766-B53971F51963}.Release|x86.ActiveCfg = Release|Any CPU
+ {828B996B-7A49-4289-A766-B53971F51963}.Release|x86.Build.0 = Release|Any CPU
{4BE7ACBC-52A6-4E21-9035-C4C9D54A92EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4BE7ACBC-52A6-4E21-9035-C4C9D54A92EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4BE7ACBC-52A6-4E21-9035-C4C9D54A92EB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4BE7ACBC-52A6-4E21-9035-C4C9D54A92EB}.Debug|x64.Build.0 = Debug|Any CPU
+ {4BE7ACBC-52A6-4E21-9035-C4C9D54A92EB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4BE7ACBC-52A6-4E21-9035-C4C9D54A92EB}.Debug|x86.Build.0 = Debug|Any CPU
{4BE7ACBC-52A6-4E21-9035-C4C9D54A92EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4BE7ACBC-52A6-4E21-9035-C4C9D54A92EB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4BE7ACBC-52A6-4E21-9035-C4C9D54A92EB}.Release|x64.ActiveCfg = Release|Any CPU
+ {4BE7ACBC-52A6-4E21-9035-C4C9D54A92EB}.Release|x64.Build.0 = Release|Any CPU
+ {4BE7ACBC-52A6-4E21-9035-C4C9D54A92EB}.Release|x86.ActiveCfg = Release|Any CPU
+ {4BE7ACBC-52A6-4E21-9035-C4C9D54A92EB}.Release|x86.Build.0 = Release|Any CPU
{A6D818B6-C085-472B-8FCC-50FD46614E54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A6D818B6-C085-472B-8FCC-50FD46614E54}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A6D818B6-C085-472B-8FCC-50FD46614E54}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A6D818B6-C085-472B-8FCC-50FD46614E54}.Debug|x64.Build.0 = Debug|Any CPU
+ {A6D818B6-C085-472B-8FCC-50FD46614E54}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A6D818B6-C085-472B-8FCC-50FD46614E54}.Debug|x86.Build.0 = Debug|Any CPU
{A6D818B6-C085-472B-8FCC-50FD46614E54}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A6D818B6-C085-472B-8FCC-50FD46614E54}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A6D818B6-C085-472B-8FCC-50FD46614E54}.Release|x64.ActiveCfg = Release|Any CPU
+ {A6D818B6-C085-472B-8FCC-50FD46614E54}.Release|x64.Build.0 = Release|Any CPU
+ {A6D818B6-C085-472B-8FCC-50FD46614E54}.Release|x86.ActiveCfg = Release|Any CPU
+ {A6D818B6-C085-472B-8FCC-50FD46614E54}.Release|x86.Build.0 = Release|Any CPU
{260EB45A-FE18-4FB3-9F1B-04A094275443}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{260EB45A-FE18-4FB3-9F1B-04A094275443}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {260EB45A-FE18-4FB3-9F1B-04A094275443}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {260EB45A-FE18-4FB3-9F1B-04A094275443}.Debug|x64.Build.0 = Debug|Any CPU
+ {260EB45A-FE18-4FB3-9F1B-04A094275443}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {260EB45A-FE18-4FB3-9F1B-04A094275443}.Debug|x86.Build.0 = Debug|Any CPU
{260EB45A-FE18-4FB3-9F1B-04A094275443}.Release|Any CPU.ActiveCfg = Release|Any CPU
{260EB45A-FE18-4FB3-9F1B-04A094275443}.Release|Any CPU.Build.0 = Release|Any CPU
+ {260EB45A-FE18-4FB3-9F1B-04A094275443}.Release|x64.ActiveCfg = Release|Any CPU
+ {260EB45A-FE18-4FB3-9F1B-04A094275443}.Release|x64.Build.0 = Release|Any CPU
+ {260EB45A-FE18-4FB3-9F1B-04A094275443}.Release|x86.ActiveCfg = Release|Any CPU
+ {260EB45A-FE18-4FB3-9F1B-04A094275443}.Release|x86.Build.0 = Release|Any CPU
{3D3152EF-C549-4B32-A4BB-6C2ED8ADDC38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D3152EF-C549-4B32-A4BB-6C2ED8ADDC38}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3D3152EF-C549-4B32-A4BB-6C2ED8ADDC38}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3D3152EF-C549-4B32-A4BB-6C2ED8ADDC38}.Debug|x64.Build.0 = Debug|Any CPU
+ {3D3152EF-C549-4B32-A4BB-6C2ED8ADDC38}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3D3152EF-C549-4B32-A4BB-6C2ED8ADDC38}.Debug|x86.Build.0 = Debug|Any CPU
{3D3152EF-C549-4B32-A4BB-6C2ED8ADDC38}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D3152EF-C549-4B32-A4BB-6C2ED8ADDC38}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3D3152EF-C549-4B32-A4BB-6C2ED8ADDC38}.Release|x64.ActiveCfg = Release|Any CPU
+ {3D3152EF-C549-4B32-A4BB-6C2ED8ADDC38}.Release|x64.Build.0 = Release|Any CPU
+ {3D3152EF-C549-4B32-A4BB-6C2ED8ADDC38}.Release|x86.ActiveCfg = Release|Any CPU
+ {3D3152EF-C549-4B32-A4BB-6C2ED8ADDC38}.Release|x86.Build.0 = Release|Any CPU
+ {831D6502-4AAD-410A-A190-6A6DAEEBBE7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {831D6502-4AAD-410A-A190-6A6DAEEBBE7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {831D6502-4AAD-410A-A190-6A6DAEEBBE7E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {831D6502-4AAD-410A-A190-6A6DAEEBBE7E}.Debug|x64.Build.0 = Debug|Any CPU
+ {831D6502-4AAD-410A-A190-6A6DAEEBBE7E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {831D6502-4AAD-410A-A190-6A6DAEEBBE7E}.Debug|x86.Build.0 = Debug|Any CPU
+ {831D6502-4AAD-410A-A190-6A6DAEEBBE7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {831D6502-4AAD-410A-A190-6A6DAEEBBE7E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {831D6502-4AAD-410A-A190-6A6DAEEBBE7E}.Release|x64.ActiveCfg = Release|Any CPU
+ {831D6502-4AAD-410A-A190-6A6DAEEBBE7E}.Release|x64.Build.0 = Release|Any CPU
+ {831D6502-4AAD-410A-A190-6A6DAEEBBE7E}.Release|x86.ActiveCfg = Release|Any CPU
+ {831D6502-4AAD-410A-A190-6A6DAEEBBE7E}.Release|x86.Build.0 = Release|Any CPU
+ {044D8336-95A6-4A61-B229-8AFFEC259922}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {044D8336-95A6-4A61-B229-8AFFEC259922}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {044D8336-95A6-4A61-B229-8AFFEC259922}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {044D8336-95A6-4A61-B229-8AFFEC259922}.Debug|x64.Build.0 = Debug|Any CPU
+ {044D8336-95A6-4A61-B229-8AFFEC259922}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {044D8336-95A6-4A61-B229-8AFFEC259922}.Debug|x86.Build.0 = Debug|Any CPU
+ {044D8336-95A6-4A61-B229-8AFFEC259922}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {044D8336-95A6-4A61-B229-8AFFEC259922}.Release|Any CPU.Build.0 = Release|Any CPU
+ {044D8336-95A6-4A61-B229-8AFFEC259922}.Release|x64.ActiveCfg = Release|Any CPU
+ {044D8336-95A6-4A61-B229-8AFFEC259922}.Release|x64.Build.0 = Release|Any CPU
+ {044D8336-95A6-4A61-B229-8AFFEC259922}.Release|x86.ActiveCfg = Release|Any CPU
+ {044D8336-95A6-4A61-B229-8AFFEC259922}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Obvs/Hosting/ObvsBuilder.cs b/Obvs/Hosting/ObvsBuilder.cs
index 9ecfebc..096525a 100644
--- a/Obvs/Hosting/ObvsBuilder.cs
+++ b/Obvs/Hosting/ObvsBuilder.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Microsoft.Extensions.DependencyInjection;
using Obvs.Configuration;
using Obvs.Types;
@@ -13,10 +14,14 @@ public sealed class ObvsBuilder
{
private readonly List _services = new List();
- internal ObvsBuilder()
+ internal ObvsBuilder(IServiceCollection services)
{
+ Services = services;
}
+ /// The container's services, for extensions that need to register their own.
+ public IServiceCollection Services { get; }
+
///
/// Adds a service whose messages all implement .
///
@@ -29,7 +34,7 @@ public ObvsBuilder AddService(string name, Action
public sealed class ObvsServiceBuilder
{
- internal ObvsServiceBuilder(string name, Type serviceMessageType)
+ internal ObvsServiceBuilder(string name, Type serviceMessageType, IServiceCollection services)
{
Name = name;
ServiceMessageType = serviceMessageType;
+ Services = services;
}
/// The service's name, unique on the bus.
@@ -20,6 +22,9 @@ internal ObvsServiceBuilder(string name, Type serviceMessageType)
/// The interface every message of this service implements.
public Type ServiceMessageType { get; }
+ /// The container's services, for transports that need to register their own.
+ public IServiceCollection Services { get; }
+
internal Func EndpointProviderFactory { get; private set; }
internal bool IsClient { get; private set; } = true;
diff --git a/Obvs/Hosting/ObvsServiceCollectionExtensions.cs b/Obvs/Hosting/ObvsServiceCollectionExtensions.cs
index 22204bb..4cca78e 100644
--- a/Obvs/Hosting/ObvsServiceCollectionExtensions.cs
+++ b/Obvs/Hosting/ObvsServiceCollectionExtensions.cs
@@ -34,7 +34,7 @@ public static IServiceCollection AddObvs(this IServiceCollection services, strin
var registrationKey = key ?? DefaultBusKey;
if (!registrations.TryGetValue(registrationKey, out var builder))
{
- builder = new ObvsBuilder();
+ builder = new ObvsBuilder(services);
registrations.Add(registrationKey, builder);
AddBus(services, key, builder);
}