diff --git a/samples/OrdersApi/Consumer.Tests/Consumer.Tests.csproj b/samples/OrdersApi/Consumer.Tests/Consumer.Tests.csproj index a88109dd..042ed50e 100644 --- a/samples/OrdersApi/Consumer.Tests/Consumer.Tests.csproj +++ b/samples/OrdersApi/Consumer.Tests/Consumer.Tests.csproj @@ -5,10 +5,10 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/samples/OrdersApi/Provider.Tests/Provider.Tests.csproj b/samples/OrdersApi/Provider.Tests/Provider.Tests.csproj index 1517e0f4..375e2ec6 100644 --- a/samples/OrdersApi/Provider.Tests/Provider.Tests.csproj +++ b/samples/OrdersApi/Provider.Tests/Provider.Tests.csproj @@ -1,13 +1,13 @@ - + net8.0 false - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/samples/OrdersApi/Provider.Tests/ProviderTests.cs b/samples/OrdersApi/Provider.Tests/ProviderTests.cs index 1feb302e..2252f0e9 100644 --- a/samples/OrdersApi/Provider.Tests/ProviderTests.cs +++ b/samples/OrdersApi/Provider.Tests/ProviderTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Text.Json; +using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; using PactNet; @@ -14,7 +15,7 @@ namespace Provider.Tests { - public class ProviderTests : IDisposable + public class ProviderTests : IAsyncLifetime { private static readonly Uri ProviderUri = new("http://localhost:5000"); @@ -36,8 +37,6 @@ public ProviderTests(ITestOutputHelper output) webBuilder.UseStartup(); }) .Build(); - - this.server.Start(); this.verifier = new PactVerifier("Orders API", new PactVerifierConfig { @@ -49,10 +48,19 @@ public ProviderTests(ITestOutputHelper output) }); } - public void Dispose() + /// + /// Called immediately after the class has been created, before it is used. + /// + public Task InitializeAsync() + { + this.server.Start(); + return Task.CompletedTask; + } + + public async Task DisposeAsync() { + await this.verifier.DisposeAsync(); this.server.Dispose(); - this.verifier.Dispose(); } [Fact] diff --git a/src/PactNet.Abstractions/PactNet.Abstractions.csproj b/src/PactNet.Abstractions/PactNet.Abstractions.csproj index c3a5209c..d64c7f4f 100644 --- a/src/PactNet.Abstractions/PactNet.Abstractions.csproj +++ b/src/PactNet.Abstractions/PactNet.Abstractions.csproj @@ -12,6 +12,7 @@ + diff --git a/src/PactNet.Abstractions/Verifier/Messaging/IMessageScenarios.cs b/src/PactNet.Abstractions/Verifier/Messaging/IMessageScenarios.cs index 58b728a6..4fc88676 100644 --- a/src/PactNet.Abstractions/Verifier/Messaging/IMessageScenarios.cs +++ b/src/PactNet.Abstractions/Verifier/Messaging/IMessageScenarios.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; namespace PactNet.Verifier.Messaging { @@ -20,6 +21,13 @@ public interface IMessageScenarios /// Message content factory IMessageScenarios Add(string description, Func factory); + /// + /// Add a message scenario + /// + /// Scenario description + /// Message content factory + IMessageScenarios Add(string description, Func> factory); + /// /// Add a message scenario by configuring a scenario builder /// diff --git a/src/PactNet.Abstractions/Verifier/Messaging/IMessagingProvider.cs b/src/PactNet.Abstractions/Verifier/Messaging/IMessagingProvider.cs index 5aa61d00..8d9955b5 100644 --- a/src/PactNet.Abstractions/Verifier/Messaging/IMessagingProvider.cs +++ b/src/PactNet.Abstractions/Verifier/Messaging/IMessagingProvider.cs @@ -6,7 +6,7 @@ namespace PactNet.Verifier.Messaging /// /// Messaging provider service, which simulates messaging responses in order to verify interactions /// - public interface IMessagingProvider : IDisposable + public interface IMessagingProvider : IAsyncDisposable { /// /// Scenarios configured for the provider diff --git a/src/PactNet.Abstractions/Verifier/Messaging/Scenario.cs b/src/PactNet.Abstractions/Verifier/Messaging/Scenario.cs index 1fe7e041..dac8cf44 100644 --- a/src/PactNet.Abstractions/Verifier/Messaging/Scenario.cs +++ b/src/PactNet.Abstractions/Verifier/Messaging/Scenario.cs @@ -1,5 +1,6 @@ using System; using System.Text.Json; +using System.Threading.Tasks; namespace PactNet.Verifier.Messaging { @@ -8,7 +9,7 @@ namespace PactNet.Verifier.Messaging /// public class Scenario { - private readonly Func factory; + private readonly Func> factory; /// /// The description of the scenario @@ -30,7 +31,16 @@ public class Scenario /// /// the scenario description /// Message content factory - public Scenario(string description, Func factory) + public Scenario(string description, Func factory) : this(description, Wrap(factory)) + { + } + + /// + /// Creates an instance of + /// + /// the scenario description + /// Message content factory + public Scenario(string description, Func> factory) { this.Description = !string.IsNullOrWhiteSpace(description) ? description : throw new ArgumentException("Description cannot be null or empty"); this.factory = factory ?? throw new ArgumentNullException(nameof(factory)); @@ -44,6 +54,20 @@ public Scenario(string description, Func factory) /// the metadata /// Custom JSON serializer settings public Scenario(string description, Func factory, dynamic metadata, JsonSerializerOptions settings) + : this(description, Wrap(factory)) + { + this.Metadata = metadata; + this.JsonSettings = settings; + } + + /// + /// Creates an instance of + /// + /// the scenario description + /// Message content factory + /// the metadata + /// Custom JSON serializer settings + public Scenario(string description, Func> factory, dynamic metadata, JsonSerializerOptions settings) : this(description, factory) { this.Metadata = metadata; @@ -51,12 +75,20 @@ public Scenario(string description, Func factory, dynamic metadata, Jso } /// - /// Invoke a scenario + /// Invoke a scenario to generate message content /// /// The scenario message content - public dynamic Invoke() + public async Task InvokeAsync() => await this.factory(); + + /// + /// Wraps a sync factory to be async + /// + /// Sync factory + /// Async factory + private static Func> Wrap(Func factory) => () => { - return this.factory.Invoke(); - } + dynamic d = factory(); + return Task.FromResult(d); + }; } } diff --git a/src/PactNet/Verifier/Messaging/MessageScenarioBuilder.cs b/src/PactNet/Verifier/Messaging/MessageScenarioBuilder.cs index 06b483f4..8db626bf 100644 --- a/src/PactNet/Verifier/Messaging/MessageScenarioBuilder.cs +++ b/src/PactNet/Verifier/Messaging/MessageScenarioBuilder.cs @@ -11,7 +11,7 @@ internal class MessageScenarioBuilder : IMessageScenarioBuilder { private readonly string description; - private Func factory; + private Func> factory; private dynamic metadata = new { ContentType = "application/json" }; private JsonSerializerOptions settings; @@ -36,23 +36,37 @@ public IMessageScenarioBuilder WithMetadata(dynamic metadata) } /// - /// Set the action of the scenario + /// Set the content factory of the scenario. The factory is invoked each time the scenario is required. /// /// Content factory public void WithContent(Func factory) { - this.factory = factory ?? throw new ArgumentNullException(nameof(factory)); + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + this.WithAsyncContent(() => Task.FromResult(factory())); } /// - /// Set the content of the scenario + /// Set the content factory of the scenario. The factory is invoked each time the scenario is required. /// /// Content factory /// Custom JSON serializer settings public void WithContent(Func factory, JsonSerializerOptions settings) { - this.factory = factory ?? throw new ArgumentNullException(nameof(factory)); - this.settings = settings ?? throw new ArgumentNullException(nameof(settings)); + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + this.WithAsyncContent(() => Task.FromResult(factory()), settings); } /// @@ -61,12 +75,7 @@ public void WithContent(Func factory, JsonSerializerOptions settings) /// Content factory public void WithAsyncContent(Func> factory) { - if (factory == null) - { - throw new ArgumentNullException(nameof(factory)); - } - - this.WithContent(() => factory().GetAwaiter().GetResult()); + this.factory = factory ?? throw new ArgumentNullException(nameof(factory)); } /// @@ -76,12 +85,8 @@ public void WithAsyncContent(Func> factory) /// Custom JSON serializer settings public void WithAsyncContent(Func> factory, JsonSerializerOptions settings) { - if (factory == null) - { - throw new ArgumentNullException(nameof(factory)); - } - - this.WithContent(() => factory().GetAwaiter().GetResult(), settings); + this.factory = factory ?? throw new ArgumentNullException(nameof(factory)); + this.settings = settings ?? throw new ArgumentNullException(nameof(settings)); } /// diff --git a/src/PactNet/Verifier/Messaging/MessageScenarios.cs b/src/PactNet/Verifier/Messaging/MessageScenarios.cs index 25b23af7..6c5df8c0 100644 --- a/src/PactNet/Verifier/Messaging/MessageScenarios.cs +++ b/src/PactNet/Verifier/Messaging/MessageScenarios.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Threading.Tasks; namespace PactNet.Verifier.Messaging { @@ -35,6 +36,18 @@ public MessageScenarios() /// Scenario description /// Message content factory public IMessageScenarios Add(string description, Func factory) + { + Func> asyncFactory = () => Task.FromResult(factory()); + + return this.Add(description, asyncFactory); + } + + /// + /// Add a message scenario + /// + /// Scenario description + /// Message content factory + public IMessageScenarios Add(string description, Func> factory) { var scenario = new Scenario(description, factory, JsonMetadata, null); this.scenarios.Add(description, scenario); diff --git a/src/PactNet/Verifier/Messaging/MessagingProvider.cs b/src/PactNet/Verifier/Messaging/MessagingProvider.cs index 5b70cef5..d112bf5d 100644 --- a/src/PactNet/Verifier/Messaging/MessagingProvider.cs +++ b/src/PactNet/Verifier/Messaging/MessagingProvider.cs @@ -7,6 +7,7 @@ using System.Text; using System.Text.Json; using System.Threading; +using System.Threading.Tasks; using PactNet.Exceptions; using PactNet.Internal; @@ -27,9 +28,10 @@ internal class MessagingProvider : IMessagingProvider }; private readonly PactVerifierConfig config; - private readonly Thread thread; + private readonly CancellationTokenSource cts; private HttpListener server; + private Task serverTask; private JsonSerializerOptions defaultSettings; /// @@ -46,7 +48,7 @@ public MessagingProvider(PactVerifierConfig config, IMessageScenarios scenarios) { this.config = config; this.Scenarios = scenarios; - this.thread = new Thread(this.HandleRequest); + this.cts = new CancellationTokenSource(); } /// @@ -59,7 +61,7 @@ public Uri Start(JsonSerializerOptions settings) Guard.NotNull(settings, nameof(settings)); this.defaultSettings = settings; - while (true) + while (!this.cts.Token.IsCancellationRequested) { Uri uri; @@ -85,9 +87,11 @@ public Uri Start(JsonSerializerOptions settings) throw new PactFailureException("Unable to start the internal messaging server", e); } - this.thread.Start(); + this.serverTask = Task.Run(this.HandleRequest, this.cts.Token); return uri; } + + return null; } /// @@ -118,21 +122,21 @@ private static int FindUnusedPort() /// /// Handle an incoming request from the Pact Core messaging driver /// - private void HandleRequest() + private async Task HandleRequest() { this.config.WriteLine("Messaging provider successfully started"); - while (this.server.IsListening) + while (this.server.IsListening && !this.cts.Token.IsCancellationRequested) { HttpListenerContext context; try { - context = this.server.GetContext(); + context = await this.server.GetContextAsync().ConfigureAwait(false); } catch (HttpListenerException) { - // this thread blocks waiting for the next request, and if the server stops then this exception is raised + // this task blocks waiting for the next request, and if the server stops then this exception is raised break; } @@ -146,8 +150,10 @@ private void HandleRequest() try { + // buffer the body instead of async deserialisation so we can log it if anything goes wrong var reader = new StreamReader(context.Request.InputStream); - string body = reader.ReadToEnd(); + string body = await reader.ReadToEndAsync().ConfigureAwait(false); + interaction = JsonSerializer.Deserialize(body, InteractionSettings); if (string.IsNullOrWhiteSpace(interaction.Description)) @@ -162,8 +168,10 @@ private void HandleRequest() continue; } - this.HandleInteraction(context, interaction); + await this.HandleInteractionAsync(context, interaction).ConfigureAwait(false); } + + this.config.WriteLine("Messaging provider stopped"); } /// @@ -171,7 +179,7 @@ private void HandleRequest() /// /// HTTP context /// Interaction - private void HandleInteraction(HttpListenerContext context, MessageInteraction interaction) + private async Task HandleInteractionAsync(HttpListenerContext context, MessageInteraction interaction) { try { @@ -194,7 +202,7 @@ private void HandleInteraction(HttpListenerContext context, MessageInteraction i this.config.WriteLine($"Metadata: {stringifyMetadata}"); } - dynamic content = scenario.Invoke(); + dynamic content = await scenario.InvokeAsync().ConfigureAwait(false); string response = JsonSerializer.Serialize(content, settings); this.OkResponse(context, response); @@ -278,19 +286,24 @@ private void WriteOutput(HttpListenerResponse response, HttpStatusCode status, s /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// - public void Dispose() + public async ValueTask DisposeAsync() { GC.SuppressFinalize(this); try { + this.cts.Cancel(false); + this.server?.Stop(); + await this.serverTask; this.server?.Close(); } catch { // ignore - we're shutting down anyway } + + this.config.WriteLine("Messaging provider disposed"); } } } diff --git a/src/PactNet/Verifier/PactVerifier.cs b/src/PactNet/Verifier/PactVerifier.cs index 09d7a0c5..32ad0705 100644 --- a/src/PactNet/Verifier/PactVerifier.cs +++ b/src/PactNet/Verifier/PactVerifier.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Text.Json; +using System.Threading.Tasks; using PactNet.Internal; using PactNet.Verifier.Messaging; @@ -9,7 +10,7 @@ namespace PactNet.Verifier /// /// Pact verifier /// - public class PactVerifier : IPactVerifier, IDisposable + public class PactVerifier : IPactVerifier, IAsyncDisposable { private const string VerifierNotInitialised = $"You must add at least one verifier transport by calling {nameof(WithHttpEndpoint)} and/or {nameof(WithMessages)}"; @@ -213,12 +214,21 @@ public IPactVerifierSource WithPactBrokerSource(Uri brokerBaseUri, Action - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// Performs application-defined tasks associated with freeing, releasing, or + /// resetting unmanaged resources asynchronously. /// - public void Dispose() + public async ValueTask DisposeAsync() { - this.messagingProvider?.Dispose(); - this.provider?.Dispose(); + if (this.provider is IAsyncDisposable providerAsyncDisposable) + { + await providerAsyncDisposable.DisposeAsync(); + } + else + { + this.provider.Dispose(); + } + + await this.messagingProvider.DisposeAsync(); } } } diff --git a/tests/PactNet.Abstractions.Tests/PactNet.Abstractions.Tests.csproj b/tests/PactNet.Abstractions.Tests/PactNet.Abstractions.Tests.csproj index 33213fec..90df8362 100644 --- a/tests/PactNet.Abstractions.Tests/PactNet.Abstractions.Tests.csproj +++ b/tests/PactNet.Abstractions.Tests/PactNet.Abstractions.Tests.csproj @@ -13,9 +13,9 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/PactNet.Abstractions.Tests/Verifier/Messaging/ScenarioTests.cs b/tests/PactNet.Abstractions.Tests/Verifier/Messaging/ScenarioTests.cs index faf9ceb0..4684e07d 100644 --- a/tests/PactNet.Abstractions.Tests/Verifier/Messaging/ScenarioTests.cs +++ b/tests/PactNet.Abstractions.Tests/Verifier/Messaging/ScenarioTests.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using FluentAssertions; using PactNet.Verifier.Messaging; using Xunit; @@ -8,12 +9,12 @@ namespace PactNet.Abstractions.Tests.Verifier.Messaging public class ScenarioTests { [Fact] - public void InvokeScenario_Should_Invoke_Scenario_And_Return_Object() + public async Task InvokeScenario_Should_Invoke_Scenario_And_Return_Object() { object expected = new { field = "value" }; var scenario = new Scenario("a scenario", () => expected); - object actual = scenario.Invoke(); + object actual = await scenario.InvokeAsync(); actual.Should().BeEquivalentTo(expected); } @@ -23,7 +24,7 @@ public void Should_Be_Able_To_Get_Description_And_Metadata() { object expectedMetadata = new { key = "vvv" }; var expectedDescription = "a scenario"; - var scenario = new Scenario(expectedDescription, () => string.Empty, expectedMetadata, null); + var scenario = new Scenario(expectedDescription, () => (dynamic)string.Empty, expectedMetadata, null); Assert.Equal(expectedMetadata, scenario.Metadata); Assert.Equal(expectedDescription, scenario.Description); @@ -35,7 +36,7 @@ public void Should_Be_Able_To_Get_Description_And_Metadata() [InlineData(" ")] public void Ctor_Should_Fail_If_Invalid_Description(string description) { - object expected = new { field = "value" }; + dynamic expected = new { field = "value" }; object expectedMetadata = new { key = "vvv" }; Action actual = () => new Scenario(description, () => expected, expectedMetadata, null); diff --git a/tests/PactNet.Tests/PactNet.Tests.csproj b/tests/PactNet.Tests/PactNet.Tests.csproj index 98f360d6..447e72da 100644 --- a/tests/PactNet.Tests/PactNet.Tests.csproj +++ b/tests/PactNet.Tests/PactNet.Tests.csproj @@ -25,10 +25,10 @@ - + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/PactNet.Tests/Verifier/Messaging/MessageScenarioBuilderTests.cs b/tests/PactNet.Tests/Verifier/Messaging/MessageScenarioBuilderTests.cs index c7969d35..7ef8fc20 100644 --- a/tests/PactNet.Tests/Verifier/Messaging/MessageScenarioBuilderTests.cs +++ b/tests/PactNet.Tests/Verifier/Messaging/MessageScenarioBuilderTests.cs @@ -42,12 +42,12 @@ public void WithMetadata_NullMetadata_ThrowsArgumentNullException() } [Fact] - public void WithContent_WhenCalled_SetsContent() + public async Task WithContent_WhenCalled_SetsContent() { object expected = new { Foo = 42 }; this.builder.WithContent(() => expected); - object actual = this.builder.Build().Invoke(); + object actual = await this.builder.Build().InvokeAsync(); actual.Should().Be(expected); } @@ -64,12 +64,12 @@ public void WithContent_WithCustomSettings_SetsSettings() } [Fact] - public void WithAsyncContent_WhenCalled_SetsContent() + public async Task WithAsyncContent_WhenCalled_SetsContent() { dynamic expected = new { Foo = 42 }; this.builder.WithAsyncContent(() => Task.FromResult(expected)); - object actual = this.builder.Build().Invoke(); + object actual = await this.builder.Build().InvokeAsync(); actual.Should().Be(expected); } diff --git a/tests/PactNet.Tests/Verifier/Messaging/MessageScenariosTests.cs b/tests/PactNet.Tests/Verifier/Messaging/MessageScenariosTests.cs index 3aafa298..a4229d2f 100644 --- a/tests/PactNet.Tests/Verifier/Messaging/MessageScenariosTests.cs +++ b/tests/PactNet.Tests/Verifier/Messaging/MessageScenariosTests.cs @@ -20,7 +20,7 @@ public MessageScenariosTests() [Fact] public void Add_SimpleScenario_AddsScenarioWithJsonMetadata() { - Func factory = () => new { Foo = 42 }; + Func> factory = () => Task.FromResult((dynamic)new { Foo = 42 }); this.scenarios.Add("description", factory); diff --git a/tests/PactNet.Tests/Verifier/Messaging/MessagingProviderTests.cs b/tests/PactNet.Tests/Verifier/Messaging/MessagingProviderTests.cs index db41fdd4..291bbf33 100644 --- a/tests/PactNet.Tests/Verifier/Messaging/MessagingProviderTests.cs +++ b/tests/PactNet.Tests/Verifier/Messaging/MessagingProviderTests.cs @@ -16,7 +16,7 @@ namespace PactNet.Tests.Verifier.Messaging { - public class MessagingProviderTests : IDisposable + public class MessagingProviderTests : IAsyncDisposable { private static readonly JsonSerializerOptions Settings = new JsonSerializerOptions { @@ -46,9 +46,9 @@ public MessagingProviderTests(ITestOutputHelper output) this.client = new HttpClient { BaseAddress = uri }; } - public void Dispose() + public ValueTask DisposeAsync() { - this.provider.Dispose(); + return this.provider.DisposeAsync(); } [Fact]