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); }