Skip to content

Wolverine 6.27.0

Choose a tag to compare

@jeremydmiller jeremydmiller released this 13 Aug 05:39
2ae20da

Wolverine 6.27.0

New: WolverineFx.Fisher

Fisher — the embedded SQLite document database and event store — is now a first-class Wolverine persistence integration, alongside Marten and Polecat.

builder.Services.AddFisher(opts => opts.Connection("Data Source=app.db"))
    .ApplyAllDatabaseChangesOnStartup()
    .IntegrateWithWolverine();

A Fisher-backed service is zero-infrastructure: no server, no container, no network. The transactional inbox/outbox, saga storage and the full aggregate handler workflow all work, and the store-agnostic [WriteModel] / [ReadModel] / [DeciderFunction] / [DcbModel] attributes run against it unchanged — the same handler code compiles and runs on any of the three stores.

Two SQLite realities shape it, both documented:

  • One writer per file. Wolverine's durability tables commit on Fisher's own connection inside Fisher's transaction. A second connection to the same file is a second writer and presents as a hang rather than an error.
  • DurabilityMode.Solo. Leader election and agent distribution need several nodes sharing one database; a Fisher store is a file.

Ancillary stores work too. AddFisherStore<T>().IntegrateWithWolverine() is supported, and [Storage(typeof(IMyStore))] routes a handler to it without naming Fisher in the consumer's source.

Not in this first release, each for a reason rather than for lack of time: multi-tenancy (Fisher's tenancy is a file per tenant), cluster durability modes, and transport schema stamping (SQLite has no schemas). See Fisher Integration.

Requires Fisher 0.6.0.

New: [DcbModel] — Dynamic Consistency Boundaries, store-agnostic

The DCB workflow joins the store-agnostic vocabulary in Wolverine core. Where [WriteModel] is about one stream, [DcbModel] spans every stream whose events match a tag query, with the store asserting at commit that no matching event landed in the meantime.

public static EventTagQuery Load(ReserveSeat command)
    => EventTagQuery.For(command.ScreeningId).Or(command.CustomerId);

public static SeatReserved Handle(ReserveSeat command, [DcbModel] SeatAvailability availability)
    => new(command.ScreeningId, command.CustomerId);

Wolverine.Marten.BoundaryModelAttribute and Wolverine.Polecat.BoundaryModelAttribute now inherit from it and behave identically — existing [BoundaryModel] code needs no change. Prefer [DcbModel] in new code.

[WriteModel] fixes

  • Required now defaults from the parameter's nullable annotation (#3916). Order order is required and gets a not-found guard; Order? order is not, and is handed to your method as null so your own null branch runs. A nullable annotation with Required = true was a contradiction that silently resolved in favour of the attribute default, making the handler's null branch dead code. Setting Required explicitly still overrides the annotation either way.

    ⚠️ Behaviour change for a handler with a nullable model parameter that relied on the implicit guard. Set Required = true explicitly to keep it.

  • [Identity] is now honoured (#3918). [DeciderFunction] always respected [Identity] on the command member; [WriteModel] did not, so the same command against the same model needed an explicit [WriteModel("...")] under one form and nothing under the other. Resolution order is now: explicit [WriteModel("orderId")], then [Identity], then {Model}Id, then id, then a strong typed id match.

Amazon SQS: oversized messages (#3926)

A message too big for SQS is no longer retried forever. SQS caps a message at 256KB and rejects a larger one with InvalidParameterValue - Message must be shorter than 262144 bytes (SenderFault: true). SenderFault: true means the identical request will fail identically forever, but Wolverine treated it as a transient send failure and re-queued it — which is why this presented as a flood of identical errors rather than one. An oversized message is now logged once and discarded.

That fix also closed a durable-endpoint hole: MarkSerializationFailureAsync only logged, so a permanently unsendable envelope stayed in the outgoing table and the durability agent re-read and re-sent it on every recovery sweep. DurableSendingAgent now deletes those rows.

New: opt-in message fragmentation. If you want an oversized message to actually get through with no extra infrastructure, an endpoint can split it across several SQS messages and reassemble it on the other side:

opts.PublishMessage<BigDocumentReceived>()
    .ToSqsQueue("documents")
    .FragmentOversizedMessages();

opts.ListenToSqsQueue("documents")
    .FragmentOversizedMessages();

Two things to know before reaching for it:

  • Claim checks remain the recommended answer. WolverineFx.ClaimCheck.AmazonS3 with an auto-offload threshold is the AWS-sanctioned pattern, has no practical size ceiling, and has none of the constraints below.
  • Reassembly is in memory, on one listener. SQS is a competing-consumer queue, so fragments of one message can scatter across nodes. Use fragmentation only on a FIFO queue, behind a globally partitioned listener, or with a single listening node. Global partitioning is the recommended shape — the fragments carry the message's GroupId, so the whole message routes to the node that owns that group.

Nothing is acknowledged until a set is complete, so a node that crashes holding part of one loses nothing — the fragments were never deleted and become visible again. See Large Messages in SQS.

Fixes

  • RabbitMQ: .Named() on a bound queue endpoint no longer breaks startup (#3915). The binding was declared from EndpointName — a logical label for logging and metrics — rather than QueueName. The two are equal until you rename an endpoint, at which point Wolverine asked the broker to bind a queue that was never declared and the application failed to start with NOT_FOUND - no queue '<endpoint name>' in vhost '/'.

  • Build: cleared the SSH.NET advisory (#3922) — GHSA-q939-rpr3-3284 against a transitive Testcontainers dependency was failing every build under TreatWarningsAsErrors.

  • The Marten and Polecat op policies now mark their chains transactional (#3911). MartenOpPolicy / PolecatOpPolicy call ApplyTransactionSupport — which ends the generated code in SaveChangesAsync — but never set IChain.IsTransactional, so any policy keying on that flag got a false negative. The same disagreement #3893 fixed for [WriteAggregate].

Internal

  • FisherTests runs in CI as its own CIFisher job. It needs no container.
  • GH-3907's aggregate handler workflow unification is complete for the DCB half: BoundaryModelAttribute and the boundary event capture frames are one implementation in core instead of one per store.