Skip to content

Commit d6579bb

Browse files
authored
v3.14.0 (#93)
* Obsolete TypedHttpClientBase retry/timeout features. * CustomEventSerializers * ExpectedEventPublisher updates. * Doco fix.
1 parent 2a26d55 commit d6579bb

29 files changed

+322
-79
lines changed

CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
Represents the **NuGet** versions.
44

5+
## v3.14.0
6+
- *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.
7+
- 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/).
8+
- 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.
9+
- 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.
10+
- *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.
11+
- *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.
12+
- *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.
13+
514
## v3.13.0
615
- *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.
716
- *Enhancement*: The `AddMappers<TAssembly>()` and `AddValidators<TAssembly>()` extension methods now also support two or three assembly specification overloads.

Common.targets

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project>
22
<PropertyGroup>
3-
<Version>3.13.0</Version>
3+
<Version>3.14.0</Version>
44
<LangVersion>preview</LangVersion>
55
<Authors>Avanade</Authors>
66
<Company>Avanade</Company>
@@ -27,6 +27,7 @@
2727
<IsPackable>true</IsPackable>
2828
<GenerateDocumentationFile>true</GenerateDocumentationFile>
2929
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
30+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
3031
</PropertyGroup>
3132

3233
<ItemGroup>
@@ -35,4 +36,4 @@
3536
<Pack>true</Pack>
3637
</None>
3738
</ItemGroup>
38-
</Project>
39+
</Project>

samples/My.Hr/My.Hr.UnitTest/EmployeeControllerTest.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ public void G100_Verify_Publish()
389389
Assert.That(imp.GetNames(), Has.Length.EqualTo(1));
390390
var e = imp.GetEvents("pendingVerifications");
391391
Assert.That(e, Has.Length.EqualTo(1));
392-
ObjectComparer.Assert(new EmployeeVerificationRequest { Name = "Wendy", Age = 38, Gender = "F" }, e[0].Value);
392+
ObjectComparer.Assert(new EmployeeVerificationRequest { Name = "Wendy", Age = 39, Gender = "F" }, e[0].Value);
393393
}
394394

395395
[Test]
@@ -398,7 +398,7 @@ public void G100_Verify_Publish_WithExpectations()
398398
using var test = ApiTester.Create<Startup>();
399399
test.UseExpectedEvents()
400400
.Controller<EmployeeController>()
401-
.ExpectDestinationEvent("pendingVerifications", new EventData<EmployeeVerificationRequest> { Value = new EmployeeVerificationRequest { Name = "Wendy", Age = 38, Gender = "F" } })
401+
.ExpectDestinationEvent("pendingVerifications", new EventData<EmployeeVerificationRequest> { Value = new EmployeeVerificationRequest { Name = "Wendy", Age = 39, Gender = "F" } })
402402
.ExpectStatusCode(System.Net.HttpStatusCode.Accepted)
403403
.Run(c => c.VerifyAsync(1.ToGuid()));
404404
}

samples/My.Hr/My.Hr.UnitTest/EmployeeControllerTest2.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ public void G100_Verify_Publish()
379379
Assert.That(imp.GetNames(), Has.Length.EqualTo(1));
380380
var e = imp.GetEvents("pendingVerifications");
381381
Assert.That(e, Has.Length.EqualTo(1));
382-
ObjectComparer.Assert(new EmployeeVerificationRequest { Name = "Wendy", Age = 38, Gender = "F" }, e[0].Value);
382+
ObjectComparer.Assert(new EmployeeVerificationRequest { Name = "Wendy", Age = 39, Gender = "F" }, e[0].Value);
383383
}
384384

385385
[Test]
@@ -388,7 +388,7 @@ public void G100_Verify_Publish_WithExpectations()
388388
using var test = ApiTester.Create<Startup>().ConfigureServices(sc => sc.ReplaceScoped<IEmployeeService, EmployeeService2>());
389389
test.UseExpectedEvents()
390390
.Controller<EmployeeController>()
391-
.ExpectDestinationEvent("pendingVerifications", new EventData<EmployeeVerificationRequest> { Value = new EmployeeVerificationRequest { Name = "Wendy", Age = 38, Gender = "F" } })
391+
.ExpectDestinationEvent("pendingVerifications", new EventData<EmployeeVerificationRequest> { Value = new EmployeeVerificationRequest { Name = "Wendy", Age = 39, Gender = "F" } })
392392
.ExpectStatusCode(System.Net.HttpStatusCode.Accepted)
393393
.Run(c => c.VerifyAsync(1.ToGuid()));
394394
}

samples/My.Hr/My.Hr.UnitTest/EmployeeResultControllerTest.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ public void G100_Verify_Publish()
392392
Assert.That(imp.GetNames(), Has.Length.EqualTo(1));
393393
var e = imp.GetEvents("pendingVerifications");
394394
Assert.That(e, Has.Length.EqualTo(1));
395-
ObjectComparer.Assert(new EmployeeVerificationRequest { Name = "Wendy", Age = 38, Gender = "F" }, e[0].Value);
395+
ObjectComparer.Assert(new EmployeeVerificationRequest { Name = "Wendy", Age = 39, Gender = "F" }, e[0].Value);
396396
}
397397

398398
[Test]
@@ -401,7 +401,7 @@ public void G100_Verify_Publish_WithExpectations()
401401
using var test = ApiTester.Create<Startup>();
402402
test.UseExpectedEvents()
403403
.Controller<EmployeeResultController>()
404-
.ExpectDestinationEvent("pendingVerifications", new EventData<EmployeeVerificationRequest> { Value = new EmployeeVerificationRequest { Name = "Wendy", Age = 38, Gender = "F" } })
404+
.ExpectDestinationEvent("pendingVerifications", new EventData<EmployeeVerificationRequest> { Value = new EmployeeVerificationRequest { Name = "Wendy", Age = 39, Gender = "F" } })
405405
.ExpectStatusCode(System.Net.HttpStatusCode.Accepted)
406406
.Run(c => c.VerifyAsync(1.ToGuid()));
407407
}

samples/My.Hr/My.Hr.UnitTest/My.Hr.UnitTest.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
<ItemGroup>
2323
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
24-
<PackageReference Include="NUnit" Version="4.0.1" />
24+
<PackageReference Include="NUnit" Version="4.1.0" />
2525
<PackageReference Include="NUnit.Analyzers" Version="4.0.1">
2626
<PrivateAssets>all</PrivateAssets>
2727
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

src/CoreEx.Newtonsoft/Json/CloudEventSerializer.cs

+16-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,21 @@ protected override Task<CloudEvent> DecodeAsync<T>(BinaryData eventData, Cancell
5050

5151
/// <inheritdoc/>
5252
protected override Task<BinaryData> EncodeAsync(CloudEvent cloudEvent, CancellationToken cancellationToken = default)
53-
=> Task.FromResult(new BinaryData(new JsonEventFormatter(JsonSerializer).EncodeStructuredModeMessage(cloudEvent, out var _)));
53+
=> Task.FromResult(new BinaryData(new InternalFormatter(JsonSerializer).EncodeStructuredModeMessage(cloudEvent, out var _)));
54+
55+
private class InternalFormatter(Nsj.JsonSerializer jsonSerializer) : JsonEventFormatter(jsonSerializer)
56+
{
57+
/// <inheritdoc/>
58+
protected override void EncodeStructuredModeData(CloudEvent cloudEvent, JsonWriter writer)
59+
{
60+
if (cloudEvent.Data is BinaryData bd && cloudEvent.DataContentType == MediaTypeNames.Application.Json)
61+
{
62+
writer.WritePropertyName(DataPropertyName);
63+
writer.WriteRawValue(bd.ToString());
64+
}
65+
else
66+
base.EncodeStructuredModeData(cloudEvent, writer);
67+
}
68+
}
5469
}
5570
}

src/CoreEx.UnitTesting.NUnit/CoreEx.UnitTesting.NUnit.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<Import Project="..\..\Common.targets" />
1313

1414
<ItemGroup>
15-
<PackageReference Include="UnitTestEx.NUnit" Version="4.1.2" />
15+
<PackageReference Include="UnitTestEx.NUnit" Version="4.2.0" />
1616
</ItemGroup>
1717

1818
<ItemGroup>

src/CoreEx.UnitTesting/CoreEx.UnitTesting.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
<ItemGroup>
2121
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
22-
<PackageReference Include="UnitTestEx" Version="4.1.2" />
22+
<PackageReference Include="UnitTestEx" Version="4.2.0" />
2323
</ItemGroup>
2424

2525
</Project>

src/CoreEx.UnitTesting/Expectations/EventExpectations.cs

+4-3
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ protected override Task OnAssertAsync(AssertArgs args)
122122
if (!expectedEventPublisher.IsEmpty)
123123
args.Tester.Implementor.AssertFail("Expected Event Publish/Send mismatch; there are one or more published events that have not been sent.");
124124

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

@@ -158,9 +158,10 @@ protected override Task OnAssertAsync(AssertArgs args)
158158
}
159159

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

165166
/// <summary>
166167
/// Asserts the events for the destination.

src/CoreEx.UnitTesting/Expectations/ExpectedEventPublisher.cs

+23-11
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
using CoreEx.Events;
55
using CoreEx.Json;
66
using Microsoft.Extensions.Logging;
7-
using System;
87
using System.Collections.Concurrent;
8+
using System.Linq;
99
using System.Text;
1010
using System.Threading;
1111
using System.Threading.Tasks;
@@ -16,7 +16,8 @@ namespace UnitTestEx.Expectations
1616
/// <summary>
1717
/// Provides an expected event publisher to support <see cref="EventExpectations{TTester}"/>.
1818
/// </summary>
19-
/// <remarks>Where an <see cref="ILogger"/> is provided then each <see cref="EventData"/> will also be logged during <i>Send</i>.</remarks>
19+
/// <remarks>Where an <see cref="ILogger"/> is provided then each <see cref="EventData"/> will also be logged during <i>Send</i>.
20+
/// <para></para></remarks>
2021
public sealed class ExpectedEventPublisher : EventPublisher
2122
{
2223
private readonly TestSharedState _sharedState;
@@ -33,15 +34,15 @@ public sealed class ExpectedEventPublisher : EventPublisher
3334
/// </summary>
3435
/// <param name="sharedState">The <see cref="TestSharedState"/>.</param>
3536
/// <returns>The <see cref="ExpectedEventPublisher"/> where found; otherwise, <c>null</c>.</returns>
36-
public static ExpectedEventPublisher? GetFromSharedState(TestSharedState sharedState)
37+
internal static ExpectedEventPublisher? GetFromSharedState(TestSharedState sharedState)
3738
=> sharedState.ThrowIfNull(nameof(sharedState)).StateData.TryGetValue(nameof(ExpectedEventPublisher), out var eep) ? eep as ExpectedEventPublisher : null;
3839

3940
/// <summary>
4041
/// Sets the <see cref="ExpectedEventPublisher"/> into the <see cref="TestSharedState"/>.
4142
/// </summary>
4243
/// <param name="sharedState">The <see cref="TestSharedState"/>.</param>
4344
/// <param name="expectedEventPublisher">The <see cref="ExpectedEventPublisher"/>.</param>
44-
public static void SetToSharedState(TestSharedState sharedState, ExpectedEventPublisher? expectedEventPublisher)
45+
internal static void SetToSharedState(TestSharedState sharedState, ExpectedEventPublisher? expectedEventPublisher)
4546
=> sharedState.ThrowIfNull(nameof(sharedState)).StateData[nameof(ExpectedEventPublisher)] = expectedEventPublisher.ThrowIfNull(nameof(expectedEventPublisher));
4647

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

6365
/// <summary>
64-
/// Gets the dictionary that contains the sent events by destination.
66+
/// Gets the dictionary that contains the actual published and sent events by destination.
6567
/// </summary>
66-
/// <remarks>The sent events are queued as the JSON-serialized representation of the <see cref="EventData"/>.</remarks>
67-
public ConcurrentDictionary<string, ConcurrentQueue<string?>> SentEvents { get; } = new();
68+
/// <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>
69+
public ConcurrentDictionary<string, ConcurrentQueue<(string Json, EventData Event, EventSendData SentEvent)>> PublishedEvents { get; } = new();
70+
71+
/// <summary>
72+
/// Indicates whether any events have been published.
73+
/// </summary>
74+
public bool HasPublishedEvents => !PublishedEvents.IsEmpty;
75+
76+
/// <summary>
77+
/// Gets the total count of published events (across all destinations).
78+
/// </summary>
79+
public int PublishedEventCount => PublishedEvents.Select(x => x.Value.Count).Sum();
6880

6981
/// <inheritdoc/>
7082
protected override Task OnEventSendAsync(string? name, EventData eventData, EventSendData eventSendData, CancellationToken cancellationToken)
7183
{
72-
var queue = SentEvents.GetOrAdd(name ?? NullKeyName, _ => new ConcurrentQueue<string?>());
84+
var queue = PublishedEvents.GetOrAdd(name ?? NullKeyName, _ => new ConcurrentQueue<(string Json, EventData Event, EventSendData SentEvent)>());
7385
var json = _jsonSerializer.Serialize(eventData, JsonWriteFormat.Indented);
74-
queue.Enqueue(json);
86+
queue.Enqueue((json, eventData, eventSendData));
7587

7688
if (_logger != null)
7789
{

src/CoreEx.UnitTesting/Expectations/UnitTestExExtensions.cs

+18-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using CoreEx.Events;
66
using CoreEx.Http;
77
using CoreEx.Wildcards;
8+
using Microsoft.Extensions.DependencyInjection;
89
using System;
910
using System.Collections.Generic;
1011
using System.Linq;
@@ -56,9 +57,23 @@ public static TSelf UseExpectedEvents<TSelf>(this TesterBase<TSelf> tester) wher
5657
/// <summary>
5758
/// Gets whether the <see cref="UseExpectedEvents{TSelf}(TesterBase{TSelf})"/> has been invoked
5859
/// </summary>
59-
/// <param name="owner"></param>
60-
/// <returns></returns>
61-
public static bool IsExpectedEventPublisherConfigured(this TesterBase owner) => owner.SetUp.Properties.TryGetValue(TesterBaseIsExpectedEventPublisherConfiguredKey, out var val) && (bool)val!;
60+
/// <param name="tester">The <see cref="TesterBase"/>.</param>
61+
/// <returns>Indicates whether the <see cref="ExpectedEventPublisher"/> is configured.</returns>
62+
public static bool IsExpectedEventPublisherConfigured(this TesterBase tester) => tester.SetUp.Properties.TryGetValue(TesterBaseIsExpectedEventPublisherConfiguredKey, out var val) && (bool)val!;
63+
64+
/// <summary>
65+
/// Gets the <see cref="ExpectedEventPublisher"/> from the <see cref="TesterBase.Services"/>.
66+
/// </summary>
67+
/// <param name="tester">The <see cref="TesterBase"/>.</param>
68+
/// <returns>The <see cref="ExpectedEventPublisher"/>.</returns>
69+
/// <exception cref="InvalidOperationException">Thrown where the <see cref="ExpectedEventPublisher"/> has not been configured (<see cref="IsExpectedEventPublisherConfigured"/>).</exception>
70+
public static ExpectedEventPublisher GetExpectedEventPublisher(this TesterBase tester)
71+
{
72+
if (!IsExpectedEventPublisherConfigured(tester))
73+
throw new InvalidOperationException($"The {nameof(ExpectedEventPublisher)} has not been configured. Please ensure that the {nameof(UseExpectedEvents)} method has been invoked.");
74+
75+
return (ExpectedEventPublisher)tester.Services.GetRequiredService<IEventPublisher>();
76+
}
6277

6378
#endregion
6479

0 commit comments

Comments
 (0)