Skip to content

Commit

Permalink
v3.14.0 (#93)
Browse files Browse the repository at this point in the history
* Obsolete TypedHttpClientBase retry/timeout features.

* CustomEventSerializers

* ExpectedEventPublisher updates.

* Doco fix.
  • Loading branch information
chullybun authored Mar 10, 2024
1 parent 2a26d55 commit d6579bb
Show file tree
Hide file tree
Showing 29 changed files with 322 additions and 79 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

Represents the **NuGet** versions.

## v3.14.0
- *Enhancement*: Planned feature obsoletion. The `TypedHttpClientBase` methods `WithRetry`, `WithTimeout`, `WithCustomRetryPolicy` and `WithMaxRetryDelay` are now marked as obsolete and will result in a compile-time warning. Related `TypedHttpClientOptions`, `HttpRequestLogger` and `SettingsBase` capabilities have also been obsoleted.
- Why? Primarily based on Microsoft guidance around [`IHttpClientFactory`](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) usage. Specifically advances in native HTTP [resilency](https://learn.microsoft.com/en-us/dotnet/core/resilience/http-resilience) support, and the [.NET 8 networking improvements](https://devblogs.microsoft.com/dotnet/dotnet-8-networking-improvements/).
- When? Soon, planned within the next minor release (`v3.15.0`). This will simplify the underlying `TypedHttpClientBase` logic and remove the internal dependency on an older version of the [_Polly_](https://www.nuget.org/packages/Polly/7.2.4) package.
- How? Review the compile-time warnings, and [update the codebase](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly) to use the native `IHttpClientFactory` resiliency capabilities.
- *Enhancement*: Updated `CoreEx.UnitTesting` to leverage the latest `UnitTestEx` (`v4.2.0`) which has added support for testing `HttpMessageHandler` and `HttpClient` configurations. This will enable improved mocked testing as a result of the above changes where applicable.
- *Enhancement*: Added `CustomSerializers` property to `IEventSerializer` of type `CustomEventSerializers`. This allows for the add (registration) of custom JSON serialization logic for a specified `EventData.Value` type. This is intended to allow an opportunity to serialize a specific type in a different manner to the default JSON serialization; for example, exclude certain properties, or use a different serialization format.
- *Enhancement*: Updated the unit testing `ExpectedEventPublisher` so that it now executes the configured `IEventSerializer` during publishing. A new `UnitTestBase.GetExpectedEventPublisher` extension method added to simplify access to the `ExpectedEventPublisher` instance and corresponding `GetPublishedEvents` property to enable further assert where required.

## v3.13.0
- *Enhancement*: Added `DatabaseMapperEx` enabling extended/explicit mapping where performance is critical versus existing that uses reflection and compiled expressions; can offer up to 40%+ improvement in some scenarios.
- *Enhancement*: The `AddMappers<TAssembly>()` and `AddValidators<TAssembly>()` extension methods now also support two or three assembly specification overloads.
Expand Down
5 changes: 3 additions & 2 deletions Common.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>3.13.0</Version>
<Version>3.14.0</Version>
<LangVersion>preview</LangVersion>
<Authors>Avanade</Authors>
<Company>Avanade</Company>
Expand All @@ -27,6 +27,7 @@
<IsPackable>true</IsPackable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
Expand All @@ -35,4 +36,4 @@
<Pack>true</Pack>
</None>
</ItemGroup>
</Project>
</Project>
4 changes: 2 additions & 2 deletions samples/My.Hr/My.Hr.UnitTest/EmployeeControllerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ public void G100_Verify_Publish()
Assert.That(imp.GetNames(), Has.Length.EqualTo(1));
var e = imp.GetEvents("pendingVerifications");
Assert.That(e, Has.Length.EqualTo(1));
ObjectComparer.Assert(new EmployeeVerificationRequest { Name = "Wendy", Age = 38, Gender = "F" }, e[0].Value);
ObjectComparer.Assert(new EmployeeVerificationRequest { Name = "Wendy", Age = 39, Gender = "F" }, e[0].Value);
}

[Test]
Expand All @@ -398,7 +398,7 @@ public void G100_Verify_Publish_WithExpectations()
using var test = ApiTester.Create<Startup>();
test.UseExpectedEvents()
.Controller<EmployeeController>()
.ExpectDestinationEvent("pendingVerifications", new EventData<EmployeeVerificationRequest> { Value = new EmployeeVerificationRequest { Name = "Wendy", Age = 38, Gender = "F" } })
.ExpectDestinationEvent("pendingVerifications", new EventData<EmployeeVerificationRequest> { Value = new EmployeeVerificationRequest { Name = "Wendy", Age = 39, Gender = "F" } })
.ExpectStatusCode(System.Net.HttpStatusCode.Accepted)
.Run(c => c.VerifyAsync(1.ToGuid()));
}
Expand Down
4 changes: 2 additions & 2 deletions samples/My.Hr/My.Hr.UnitTest/EmployeeControllerTest2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ public void G100_Verify_Publish()
Assert.That(imp.GetNames(), Has.Length.EqualTo(1));
var e = imp.GetEvents("pendingVerifications");
Assert.That(e, Has.Length.EqualTo(1));
ObjectComparer.Assert(new EmployeeVerificationRequest { Name = "Wendy", Age = 38, Gender = "F" }, e[0].Value);
ObjectComparer.Assert(new EmployeeVerificationRequest { Name = "Wendy", Age = 39, Gender = "F" }, e[0].Value);
}

[Test]
Expand All @@ -388,7 +388,7 @@ public void G100_Verify_Publish_WithExpectations()
using var test = ApiTester.Create<Startup>().ConfigureServices(sc => sc.ReplaceScoped<IEmployeeService, EmployeeService2>());
test.UseExpectedEvents()
.Controller<EmployeeController>()
.ExpectDestinationEvent("pendingVerifications", new EventData<EmployeeVerificationRequest> { Value = new EmployeeVerificationRequest { Name = "Wendy", Age = 38, Gender = "F" } })
.ExpectDestinationEvent("pendingVerifications", new EventData<EmployeeVerificationRequest> { Value = new EmployeeVerificationRequest { Name = "Wendy", Age = 39, Gender = "F" } })
.ExpectStatusCode(System.Net.HttpStatusCode.Accepted)
.Run(c => c.VerifyAsync(1.ToGuid()));
}
Expand Down
4 changes: 2 additions & 2 deletions samples/My.Hr/My.Hr.UnitTest/EmployeeResultControllerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ public void G100_Verify_Publish()
Assert.That(imp.GetNames(), Has.Length.EqualTo(1));
var e = imp.GetEvents("pendingVerifications");
Assert.That(e, Has.Length.EqualTo(1));
ObjectComparer.Assert(new EmployeeVerificationRequest { Name = "Wendy", Age = 38, Gender = "F" }, e[0].Value);
ObjectComparer.Assert(new EmployeeVerificationRequest { Name = "Wendy", Age = 39, Gender = "F" }, e[0].Value);
}

[Test]
Expand All @@ -401,7 +401,7 @@ public void G100_Verify_Publish_WithExpectations()
using var test = ApiTester.Create<Startup>();
test.UseExpectedEvents()
.Controller<EmployeeResultController>()
.ExpectDestinationEvent("pendingVerifications", new EventData<EmployeeVerificationRequest> { Value = new EmployeeVerificationRequest { Name = "Wendy", Age = 38, Gender = "F" } })
.ExpectDestinationEvent("pendingVerifications", new EventData<EmployeeVerificationRequest> { Value = new EmployeeVerificationRequest { Name = "Wendy", Age = 39, Gender = "F" } })
.ExpectStatusCode(System.Net.HttpStatusCode.Accepted)
.Run(c => c.VerifyAsync(1.ToGuid()));
}
Expand Down
2 changes: 1 addition & 1 deletion samples/My.Hr/My.Hr.UnitTest/My.Hr.UnitTest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NUnit" Version="4.0.1" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
17 changes: 16 additions & 1 deletion src/CoreEx.Newtonsoft/Json/CloudEventSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ protected override Task<CloudEvent> DecodeAsync<T>(BinaryData eventData, Cancell

/// <inheritdoc/>
protected override Task<BinaryData> EncodeAsync(CloudEvent cloudEvent, CancellationToken cancellationToken = default)
=> Task.FromResult(new BinaryData(new JsonEventFormatter(JsonSerializer).EncodeStructuredModeMessage(cloudEvent, out var _)));
=> Task.FromResult(new BinaryData(new InternalFormatter(JsonSerializer).EncodeStructuredModeMessage(cloudEvent, out var _)));

private class InternalFormatter(Nsj.JsonSerializer jsonSerializer) : JsonEventFormatter(jsonSerializer)
{
/// <inheritdoc/>
protected override void EncodeStructuredModeData(CloudEvent cloudEvent, JsonWriter writer)
{
if (cloudEvent.Data is BinaryData bd && cloudEvent.DataContentType == MediaTypeNames.Application.Json)
{
writer.WritePropertyName(DataPropertyName);
writer.WriteRawValue(bd.ToString());
}
else
base.EncodeStructuredModeData(cloudEvent, writer);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<Import Project="..\..\Common.targets" />

<ItemGroup>
<PackageReference Include="UnitTestEx.NUnit" Version="4.1.2" />
<PackageReference Include="UnitTestEx.NUnit" Version="4.2.0" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/CoreEx.UnitTesting/CoreEx.UnitTesting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

<ItemGroup>
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="UnitTestEx" Version="4.1.2" />
<PackageReference Include="UnitTestEx" Version="4.2.0" />
</ItemGroup>

</Project>
7 changes: 4 additions & 3 deletions src/CoreEx.UnitTesting/Expectations/EventExpectations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ protected override Task OnAssertAsync(AssertArgs args)
if (!expectedEventPublisher.IsEmpty)
args.Tester.Implementor.AssertFail("Expected Event Publish/Send mismatch; there are one or more published events that have not been sent.");

var names = expectedEventPublisher.SentEvents.Keys.ToArray();
var names = expectedEventPublisher.PublishedEvents.Keys.ToArray();
if (_expectNoEvents && !_expectEvents && _expectedEvents.Count == 0 && names.Length > 0)
args.Tester.Implementor.AssertFail($"Expected no Event(s); one or more were published.");

Expand Down Expand Up @@ -158,9 +158,10 @@ protected override Task OnAssertAsync(AssertArgs args)
}

/// <summary>
/// Gets the event from the event storage.
/// Gets the event JSON from the event storage.
/// </summary>
private static List<string?> GetEvents(ExpectedEventPublisher expectedEventPublisher, string? name) => expectedEventPublisher!.SentEvents.TryGetValue(name ?? ExpectedEventPublisher.NullKeyName, out var queue) ? [.. queue] : new();
private static List<string> GetEvents(ExpectedEventPublisher expectedEventPublisher, string? name)
=> expectedEventPublisher!.PublishedEvents.TryGetValue(name ?? ExpectedEventPublisher.NullKeyName, out var queue) ? [.. queue.Select(x => x.Json)] : new();

/// <summary>
/// Asserts the events for the destination.
Expand Down
34 changes: 23 additions & 11 deletions src/CoreEx.UnitTesting/Expectations/ExpectedEventPublisher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
using CoreEx.Events;
using CoreEx.Json;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -16,7 +16,8 @@ namespace UnitTestEx.Expectations
/// <summary>
/// Provides an expected event publisher to support <see cref="EventExpectations{TTester}"/>.
/// </summary>
/// <remarks>Where an <see cref="ILogger"/> is provided then each <see cref="EventData"/> will also be logged during <i>Send</i>.</remarks>
/// <remarks>Where an <see cref="ILogger"/> is provided then each <see cref="EventData"/> will also be logged during <i>Send</i>.
/// <para></para></remarks>
public sealed class ExpectedEventPublisher : EventPublisher
{
private readonly TestSharedState _sharedState;
Expand All @@ -33,15 +34,15 @@ public sealed class ExpectedEventPublisher : EventPublisher
/// </summary>
/// <param name="sharedState">The <see cref="TestSharedState"/>.</param>
/// <returns>The <see cref="ExpectedEventPublisher"/> where found; otherwise, <c>null</c>.</returns>
public static ExpectedEventPublisher? GetFromSharedState(TestSharedState sharedState)
internal static ExpectedEventPublisher? GetFromSharedState(TestSharedState sharedState)
=> sharedState.ThrowIfNull(nameof(sharedState)).StateData.TryGetValue(nameof(ExpectedEventPublisher), out var eep) ? eep as ExpectedEventPublisher : null;

/// <summary>
/// Sets the <see cref="ExpectedEventPublisher"/> into the <see cref="TestSharedState"/>.
/// </summary>
/// <param name="sharedState">The <see cref="TestSharedState"/>.</param>
/// <param name="expectedEventPublisher">The <see cref="ExpectedEventPublisher"/>.</param>
public static void SetToSharedState(TestSharedState sharedState, ExpectedEventPublisher? expectedEventPublisher)
internal static void SetToSharedState(TestSharedState sharedState, ExpectedEventPublisher? expectedEventPublisher)
=> sharedState.ThrowIfNull(nameof(sharedState)).StateData[nameof(ExpectedEventPublisher)] = expectedEventPublisher.ThrowIfNull(nameof(expectedEventPublisher));

/// <summary>
Expand All @@ -51,8 +52,9 @@ public static void SetToSharedState(TestSharedState sharedState, ExpectedEventPu
/// <param name="logger">The optional <see cref="ILogger"/> for logging the events (each <see cref="EventData"/>).</param>
/// <param name="jsonSerializer">The optional <see cref="IJsonSerializer"/> for the logging. Defaults to <see cref="JsonSerializer.Default"/></param>
/// <param name="eventDataFormatter">The <see cref="EventDataFormatter"/>; defaults where not specified.</param>
public ExpectedEventPublisher(TestSharedState sharedState, ILogger<ExpectedEventPublisher>? logger = null, IJsonSerializer? jsonSerializer = null, EventDataFormatter? eventDataFormatter = null)
: base(eventDataFormatter, new CoreEx.Text.Json.EventDataSerializer(), new NullEventSender())
/// <param name="eventSerializer">The <see cref="IEventSerializer"/>; defaults where not specified.</param>
public ExpectedEventPublisher(TestSharedState sharedState, ILogger<ExpectedEventPublisher>? logger = null, IJsonSerializer? jsonSerializer = null, EventDataFormatter? eventDataFormatter = null, IEventSerializer? eventSerializer = null)
: base(eventDataFormatter, eventSerializer ?? new CoreEx.Text.Json.EventDataSerializer(), new NullEventSender())
{
_sharedState = sharedState.ThrowIfNull(nameof(sharedState));
SetToSharedState(_sharedState, this);
Expand All @@ -61,17 +63,27 @@ public ExpectedEventPublisher(TestSharedState sharedState, ILogger<ExpectedEvent
}

/// <summary>
/// Gets the dictionary that contains the sent events by destination.
/// Gets the dictionary that contains the actual published and sent events by destination.
/// </summary>
/// <remarks>The sent events are queued as the JSON-serialized representation of the <see cref="EventData"/>.</remarks>
public ConcurrentDictionary<string, ConcurrentQueue<string?>> SentEvents { get; } = new();
/// <remarks>The actual published events are queued as the JSON-serialized (indented) representation of the <see cref="EventData"/>, the <see cref="EventData"/> itself, and the corresponding <see cref="EventSendData"/>.</remarks>
public ConcurrentDictionary<string, ConcurrentQueue<(string Json, EventData Event, EventSendData SentEvent)>> PublishedEvents { get; } = new();

/// <summary>
/// Indicates whether any events have been published.
/// </summary>
public bool HasPublishedEvents => !PublishedEvents.IsEmpty;

/// <summary>
/// Gets the total count of published events (across all destinations).
/// </summary>
public int PublishedEventCount => PublishedEvents.Select(x => x.Value.Count).Sum();

/// <inheritdoc/>
protected override Task OnEventSendAsync(string? name, EventData eventData, EventSendData eventSendData, CancellationToken cancellationToken)
{
var queue = SentEvents.GetOrAdd(name ?? NullKeyName, _ => new ConcurrentQueue<string?>());
var queue = PublishedEvents.GetOrAdd(name ?? NullKeyName, _ => new ConcurrentQueue<(string Json, EventData Event, EventSendData SentEvent)>());
var json = _jsonSerializer.Serialize(eventData, JsonWriteFormat.Indented);
queue.Enqueue(json);
queue.Enqueue((json, eventData, eventSendData));

if (_logger != null)
{
Expand Down
21 changes: 18 additions & 3 deletions src/CoreEx.UnitTesting/Expectations/UnitTestExExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using CoreEx.Events;
using CoreEx.Http;
using CoreEx.Wildcards;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
Expand Down Expand Up @@ -56,9 +57,23 @@ public static TSelf UseExpectedEvents<TSelf>(this TesterBase<TSelf> tester) wher
/// <summary>
/// Gets whether the <see cref="UseExpectedEvents{TSelf}(TesterBase{TSelf})"/> has been invoked
/// </summary>
/// <param name="owner"></param>
/// <returns></returns>
public static bool IsExpectedEventPublisherConfigured(this TesterBase owner) => owner.SetUp.Properties.TryGetValue(TesterBaseIsExpectedEventPublisherConfiguredKey, out var val) && (bool)val!;
/// <param name="tester">The <see cref="TesterBase"/>.</param>
/// <returns>Indicates whether the <see cref="ExpectedEventPublisher"/> is configured.</returns>
public static bool IsExpectedEventPublisherConfigured(this TesterBase tester) => tester.SetUp.Properties.TryGetValue(TesterBaseIsExpectedEventPublisherConfiguredKey, out var val) && (bool)val!;

/// <summary>
/// Gets the <see cref="ExpectedEventPublisher"/> from the <see cref="TesterBase.Services"/>.
/// </summary>
/// <param name="tester">The <see cref="TesterBase"/>.</param>
/// <returns>The <see cref="ExpectedEventPublisher"/>.</returns>
/// <exception cref="InvalidOperationException">Thrown where the <see cref="ExpectedEventPublisher"/> has not been configured (<see cref="IsExpectedEventPublisherConfigured"/>).</exception>
public static ExpectedEventPublisher GetExpectedEventPublisher(this TesterBase tester)
{
if (!IsExpectedEventPublisherConfigured(tester))
throw new InvalidOperationException($"The {nameof(ExpectedEventPublisher)} has not been configured. Please ensure that the {nameof(UseExpectedEvents)} method has been invoked.");

return (ExpectedEventPublisher)tester.Services.GetRequiredService<IEventPublisher>();
}

#endregion

Expand Down
Loading

0 comments on commit d6579bb

Please sign in to comment.