Depends on: #4149 (Newtonsoft compatibility), #4150 (RoutingKey/AMQP), #4151 (DictionaryKeyPolicy on bag).
The fix proposed here will not deliver value until those three land, because the scheduler is otherwise unusable for any non-trivial request from a Newtonsoft-backed app — failures from those issues mask this one.
Describe the bug
AzureServiceBusScheduler.ScheduleAsync<TRequest>(TRequest, RequestSchedulerType.Post, ...) serialises the request via System.Text.Json into an intermediate FireAzureScheduler.RequestData envelope. At fire time the consumer STJ-deserialises that envelope and then re-maps the request via the registered (Newtonsoft) IAmAMessageMapper<TRequest> for the wire.
For request types containing readonly record structs with explicit constructors that STJ cannot reconstruct cleanly, fields are silently zeroed. Concrete example: a value object
public readonly record struct ClientActionId(ClientActionType Type, string Value);
round-trips with Type = default(ClientActionType) = 0. The bug is silent — no exception, just wrong data on the consumer.
The scheduler ignores the user-registered Newtonsoft mapper even when one is available, despite the fact that it must invoke that same mapper at fire time anyway.
To Reproduce
- Register a Newtonsoft-based
IAmAMessageMapper<MyRequest> and matching Publication.
- Include a property on
MyRequest whose type is a readonly record struct with an explicit constructor (any type STJ deserialises to default).
- Schedule via
commandProcessor.SendAsync(TimeSpan, myRequest).
- Observe the fired message: the affected field is
default, not the value originally scheduled.
Suggested fix
When a Publication and IAmAMessageMapper<TRequest> are registered for TRequest, skip the STJ RequestData round-trip entirely:
- Map the request to a
Message using the registered mapper.
- Wrap that
Message directly in FireAzureScheduler.Message (the same envelope path used by Schedule(Message, ...)).
- Fall back to the current STJ
RequestData flow only when no mapper is registered.
This guarantees the scheduler uses the same serialisation path the application configured for normal publishes, so anything that round-trips correctly on a normal Post will also round-trip correctly on a scheduled Post.
Further technical details
- Brighter version: 10.4.1 (
Paramore.Brighter.MessageScheduler.Azure)
dotnet --info:
.NET SDK 10.0.108 (commit 94ea82652c, MSBuild 18.0.11+94ea82652)
Host 10.0.8 (x64)
RID: win-x64
- OS: Windows 11 (build 10.0.26200)
Describe the bug
AzureServiceBusScheduler.ScheduleAsync<TRequest>(TRequest, RequestSchedulerType.Post, ...)serialises the request viaSystem.Text.Jsoninto an intermediateFireAzureScheduler.RequestDataenvelope. At fire time the consumer STJ-deserialises that envelope and then re-maps the request via the registered (Newtonsoft)IAmAMessageMapper<TRequest>for the wire.For request types containing readonly record structs with explicit constructors that STJ cannot reconstruct cleanly, fields are silently zeroed. Concrete example: a value object
round-trips with
Type = default(ClientActionType) = 0. The bug is silent — no exception, just wrong data on the consumer.The scheduler ignores the user-registered Newtonsoft mapper even when one is available, despite the fact that it must invoke that same mapper at fire time anyway.
To Reproduce
IAmAMessageMapper<MyRequest>and matchingPublication.MyRequestwhose type is a readonly record struct with an explicit constructor (any type STJ deserialises todefault).commandProcessor.SendAsync(TimeSpan, myRequest).default, not the value originally scheduled.Suggested fix
When a
PublicationandIAmAMessageMapper<TRequest>are registered forTRequest, skip the STJRequestDataround-trip entirely:Messageusing the registered mapper.Messagedirectly inFireAzureScheduler.Message(the same envelope path used bySchedule(Message, ...)).RequestDataflow only when no mapper is registered.This guarantees the scheduler uses the same serialisation path the application configured for normal publishes, so anything that round-trips correctly on a normal
Postwill also round-trip correctly on a scheduledPost.Further technical details
Paramore.Brighter.MessageScheduler.Azure)dotnet --info: