Skip to content

Post-flavoured request schedules round-trip through System.Text.Json and silently corrupt readonly record structs #4152

Description

@Jonny-Freemarket

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

  1. Register a Newtonsoft-based IAmAMessageMapper<MyRequest> and matching Publication.
  2. Include a property on MyRequest whose type is a readonly record struct with an explicit constructor (any type STJ deserialises to default).
  3. Schedule via commandProcessor.SendAsync(TimeSpan, myRequest).
  4. 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:

  1. Map the request to a Message using the registered mapper.
  2. Wrap that Message directly in FireAzureScheduler.Message (the same envelope path used by Schedule(Message, ...)).
  3. 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)

Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions