Migration Guide
Key Changes in 4.0.0
Polecat 4.0 ships in lockstep with Marten 9.0 and JasperFx 2.0 as part of the Critter Stack 2026 release wave. The 4.0 line builds on the same JasperFx 2.0 / JasperFx.Events 2.0 / Weasel 9.0 release-candidate matrix that Marten 9 consumes — this is the consolidation cycle, not a rewrite. Most upgrades are pin bumps; the breaking surface is small and localized, and the bulk of the rc-line dedup work is source-compatible for typical applications (see rc dedup-wave relocations).
Foundation pin bumps
Polecat 4 consumes the 2026-wave release-candidate line for the shared substrate. Update your Directory.Packages.props (or the equivalent in your csproj):
| Package | 3.x | 4.0 (current rc) |
|---|---|---|
JasperFx | 1.31.0 | 2.0.0-rc.3 |
JasperFx.Events | 1.36.0 | 2.0.0-rc.3 |
JasperFx.Events.SourceGenerator | 1.4.0 | 2.0.0-rc.3 |
JasperFx.RuntimeCompiler (transitive, but pin centrally for matrix coherence) | (no equivalent) | 5.0.0-rc.3 |
JasperFx.SourceGeneration (transitive, but pin centrally) | (no equivalent) | 2.0.0-rc.3 |
Weasel.SqlServer | 8.15.2 | 9.0.0-alpha.8 |
Weasel.EntityFrameworkCore | 8.15.2 | 9.0.0-alpha.8 |
Polecat 4 itself is at 4.0.0-rc.2 (the consolidation wave converged through an alpha series into the rc line). The five JasperFx.* packages move as a matrix and must be pinned to the same rc together; the two Weasel.* packages share their own version and currently trail one wave behind on alpha.8 (the rc.3 line resolves cleanly against them). Keep all seven in one renovate/dependabot group. Expect a final tick to the matched 9.0/2.0 GA set as the 2026 wave closes out.
WARNING
Pin the 5.x line of JasperFx.RuntimeCompiler — that's the active continuation of the 4.x lineage. A parallel 2.0.x series exists on NuGet but is stale; don't pin against it.
Target frameworks (net9.0;net10.0) are unchanged from late Polecat 3.x; .NET 8 was already dropped before Polecat 3.2.
Dedup audit relocations
The Marten ↔ Polecat dedup audit (JasperFx/jasperfx#218) consolidated several enums that had parallel definitions in Polecat and Marten into the canonical Weasel / JasperFx homes. The relocations are mechanical — same values, different namespace.
Polecat.BulkInsertMode → Weasel.Core.BulkInsertMode
#50, audit row weasel#264.
Third-party consumers that referenced Polecat.BulkInsertMode by full name need to update their using directive:
// before (Polecat 3.x)
using Polecat;
await store.Advanced.BulkInsertAsync(docs, BulkInsertMode.OverwriteExisting);
// after (Polecat 4.0)
using Weasel.Core;
await store.Advanced.BulkInsertAsync(docs, BulkInsertMode.OverwriteExisting);The enum gained a fourth value — OverwriteIfVersionMatches — that did not exist on the Polecat 3.x enum. See the new-behavior section below.
Polecat.Storage.CascadeAction → Weasel.Core.CascadeAction
#47 / #61, audit row 2 in JasperFx/jasperfx#218.
DocumentForeignKey.OnDelete is now typed as Weasel.Core.CascadeAction. The same four values (NoAction, Cascade, SetNull, SetDefault) remain available; Weasel.Core's version additionally exposes Restrict. Existing call sites only need a using Weasel.Core; addition:
// before (Polecat 3.x)
mapping.ForeignKey<User>(x => x.AssigneeId, fk => fk.OnDelete = CascadeAction.Cascade);
// ^ resolved Polecat.Storage.CascadeAction
// after (Polecat 4.0) — add `using Weasel.Core;` to the file
mapping.ForeignKey<User>(x => x.AssigneeId, fk => fk.OnDelete = CascadeAction.Cascade);
// ^ resolves Weasel.Core.CascadeActionPolecat.Metadata.ITenanted now extends JasperFx.MultiTenancy.IHasTenantId
Multi-tenancy dedup audit slice (jasperfx#224, row 1). Polecat.Metadata.ITenanted no longer declares its own TenantId property — it inherits it from IHasTenantId. Polecat-side framework code that accepts IHasTenantId now accepts any ITenanted document type.
Source-compatible for the typical case (your document class implementing ITenanted keeps a single string TenantId { get; set; } property and compiles unchanged). Only affects code that referenced the explicit ITenanted.TenantId property symbol via reflection or interface forwarding.
Polecat.Exceptions.UnknownTenantException → JasperFx.MultiTenancy.UnknownTenantIdException
Multi-tenancy dedup audit slice (jasperfx#224, row 2). The local Polecat.Exceptions.UnknownTenantException was removed; throw sites and consumers use the canonical JasperFx.MultiTenancy.UnknownTenantIdException. In Polecat 2.0.0-alpha.7 the JasperFx exception gained a TenantId property so consumers can catch (UnknownTenantIdException ex) { ex.TenantId } without parsing the message string.
// before (Polecat 3.x)
catch (Polecat.Exceptions.UnknownTenantException ex)
{
var id = ex.TenantId;
}
// after (Polecat 4.0)
catch (JasperFx.MultiTenancy.UnknownTenantIdException ex)
{
var id = ex.TenantId;
}rc dedup-wave relocations
The rc.1 → rc.3 cycle finished the Critter Stack 2026 dedup pillar: a batch of types that Polecat and Marten each declared in parallel were consolidated into the canonical JasperFx / Weasel homes (Polecat #125–#141). The values, members, and ordinals are unchanged — only the namespace moved.
Inside Polecat the old unqualified names keep resolving (the assembly carries global using aliases / empty inheriting interfaces), so the framework's own API surface is unaffected. The break, if any, is in your code: if you referenced one of these types by its old Polecat.* name — most often by implementing a marker interface on a document class, or by a fully-qualified catch / config reference — update the using directive (or the fully-qualified name) to the new home.
| Old name (Polecat 3.x / early 4.0) | New canonical home | PR |
|---|---|---|
TenancyStyle (inline in StoreOptions) | JasperFx.MultiTenancy.TenancyStyle | #127 |
Polecat.Metadata.DeleteStyle | JasperFx.DeleteStyle | #127 |
Polecat.Events.Dcb.DcbConcurrencyException | JasperFx.Events.DcbConcurrencyException | #128 |
Polecat.Exceptions.ProgressionProgressOutOfOrderException | JasperFx.Events.Daemon.ProgressionProgressOutOfOrderException | #128 |
Polecat.Metadata.ISoftDeleted | JasperFx.Metadata.ISoftDeleted | #130 |
Polecat.Metadata.IVersioned | JasperFx.Metadata.IVersioned | #130 |
Polecat.Metadata.ITracked ⚠️ | JasperFx.Metadata.ITracked (non-nullable members — see below) | #130 |
Polecat.Patching.RemoveAction | JasperFx.Events.RemoveAction | #131 |
Polecat.Patching.IPatchExpression<T> ⚠️ | JasperFx.Events.IPatchExpression<T> (predicate overloads — see below) | #131 |
Polecat.Internal.OpenTelemetry.TrackLevel | JasperFx.OpenTelemetry.TrackLevel | #132 |
Polecat.Attributes.IdentityAttribute (document [Identity] — marks a non-Id identity member on a plain document) | JasperFx.IdentityAttribute | #135 |
Polecat.Schema.Identity.Sequences.{ISequence, HiloSettings, IReadOnlyHiloSettings} | Weasel.Core.Sequences.* | #137 |
Polecat.Serialization.{Casing, CollectionStorage, NonPublicMembersStorage, EnumStorage} | Weasel.Core.* | #137 |
Polecat.Exceptions.DocumentAlreadyExistsException ⚠️ | JasperFx.DocumentAlreadyExistsException (message text — see below) | #140 |
The two you are most likely to touch directly are the document marker interfaces (ISoftDeleted / IVersioned / ITracked — you implement these on your own document classes) and the serialization-config enums (Casing / CollectionStorage / NonPublicMembersStorage / EnumStorage, set via StoreOptions). Both just need their using swapped:
// before (Polecat 3.x)
using Polecat.Metadata;
public class Invoice : ISoftDeleted { /* ... */ }
// after (Polecat 4.0)
using JasperFx.Metadata;
public class Invoice : ISoftDeleted { /* ... */ }Three of the relocations carry a behavior change beyond the namespace move:
ITracked members are now non-nullable
#130. The lifted JasperFx.Metadata.ITracked declares CorrelationId / CausationId / LastModifiedBy as non-nullable string (Marten's canonical shape); Polecat's old interface declared them string?. If your document class declared those members nullable to match the old interface, the compiler now warns (CS8766) that the nullability doesn't line up with the interface. Drop the ? (and add a default) on the implementing class — the session always copies non-null values onto them:
// before
public string? CorrelationId { get; set; }
// after
public string CorrelationId { get; set; } = string.Empty;IPatchExpression<T> predicate overloads throw on SQL Server
#131. Polecat adopted Marten's superset IPatchExpression<T>, which adds predicate-based overloads — AppendIfNotExists(expr, element, predicate), InsertIfNotExists(expr, element, predicate, index), Remove(expr, predicate, action) — and the object-shaped Rename(string, Expression<Func<T, object>>) (replacing Polecat's generic Rename<TElement>). The predicate variants require translating the predicate into a JSON-array search, which Polecat's JSON_MODIFY patch translation does not implement yet, so they throw NotSupportedException with a message pointing at the supported non-predicate overload. The element/key/index overloads you were already using are unchanged; only the new predicate forms are unimplemented.
DocumentAlreadyExistsException message text changed
#140. The lifted JasperFx.DocumentAlreadyExistsException keeps Polecat's DocumentType (Type) and Id properties and the (Type, object) ctor, so the duplicate-key catch path is a near-no-op rebind. But the thrown Message now uses Marten's FullName-based format — "Document already exists {FullName}: {id}" (was "A document of type '{Name}' with id '{id}' already exists."). If you assert on or parse .Message, update the expectation; assertions on .DocumentType / .Id are unaffected.
Event-sourcing API changes
Projections and self-aggregating documents must be partial
JasperFx/jasperfx#276 (FEC elimination, Phase 1) removed the FastExpressionCompiler fallback in JasperFx.Events's projection apply-method dispatch. Source-generated dispatchers emitted by JasperFx.Events.SourceGenerator are now the only path — runtime registration fails fast when no [GeneratedEvolver] is found for a projection that uses conventional Apply / Create / ShouldDelete methods:
JasperFx.Events.Projections.InvalidProjectionException:
No source-generated dispatcher found for MyApp.MyProjection.
When using conventional Apply/Create/ShouldDelete methods, the projection class must be
declared `partial` in an assembly that references the JasperFx.Events.SourceGenerator analyzer,
or alternatively override Evolve / EvolveAsync / DetermineAction / DetermineActionAsync directly.To clear this exception, every projection class that uses conventional apply-method discovery must be partial, including:
- Subclasses of
SingleStreamProjection<TDoc, TId>,MultiStreamProjection<TDoc, TId>,EventProjection, andPolecatCompositeProjection. - Self-aggregating document types registered via
opts.Projections.Snapshot<T>(...), queried viasession.Events.AggregateStreamAsync<T>(...), or live-projected viasession.Events.FetchLatest<T>(...)/FetchForWriting<T>(...). The source generator emits a standaloneTEvolverclass for the closedSingleStreamProjection<T, T.IdType>runtime instance, and it can only do that whenTispartial.
// before (Polecat 3.x — works because of FEC fallback)
public class QuestParty
{
public Guid Id { get; set; }
public List<string> Members { get; } = [];
public static QuestParty Create(QuestStarted e) => new();
public void Apply(MembersJoined e) => Members.AddRange(e.Members);
}
// after (Polecat 4.0 — `partial` required)
public partial class QuestParty
{
public Guid Id { get; set; }
public List<string> Members { get; } = [];
public static QuestParty Create(QuestStarted e) => new();
public void Apply(MembersJoined e) => Members.AddRange(e.Members);
}Every assembly that defines such a projection or aggregate also needs to reference the JasperFx.Events.SourceGenerator NuGet as an analyzer-only package — Polecat itself already pulls it in through JasperFx.Events for projection types declared in your store-host project, but any sibling assembly that declares its own projection / aggregate types needs its own reference:
<ItemGroup>
<PackageReference Include="JasperFx.Events.SourceGenerator"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>If you have a projection that cannot be made partial (e.g. a sealed class from a third-party library, or one you want to keep as a non-source-generator path), the escape hatch is to override the dispatch methods directly:
public class MyProjection : SingleStreamProjection<MyAggregate, Guid>
{
public override ValueTask<MyAggregate?> EvolveAsync(
MyAggregate? snapshot, Guid id, IEvent e, CancellationToken ct)
{
// hand-written dispatch — no SG, no FEC
}
}This matches Marten 9's adoption story for the same JasperFx.Events Phase-1 change. The shared consumer-side contract is documented at greater length in Marten's migration guide — "Projection apply dispatch is now source-generator-only"; the Polecat-side adoption tracking lives in Polecat#46, with the audit harness from #110 standing as the pre-release gate against future SG regressions.
Convention-method visibility, identity, and required members
The source generator inspects only public instance / static methods. Apply / Create / ShouldDelete methods that were private / internal / protected under Polecat 3.x (the reflective FEC path picked those up) are silently skipped by the SG and trip the fail-fast above. Make them public:
// before (Polecat 3.x — FEC reflected over non-public members too)
public partial class Quest
{
public Guid Id { get; set; }
private void Apply(MembersJoined e) { /* ... */ } // <-- skipped by SG
}
// after (Polecat 4.0)
public partial class Quest
{
public Guid Id { get; set; }
public void Apply(MembersJoined e) { /* ... */ } // <-- discovered
}Aggregate identity discovery follows the same convention rule as Marten — by default, the SG looks for a property literally named Id (or <TypeName>Id). If your aggregate's identity uses a different member name, annotate that member with [Identity] from JasperFx (the same JasperFx.IdentityAttribute Polecat uses for document identity — see the rc dedup-wave relocations table; it moved out of Polecat.Attributes in #135):
using JasperFx; // JasperFx.IdentityAttribute — NOT JasperFx.Events
public partial class Quest
{
[Identity]
public Guid QuestKey { get; set; } // <-- explicit identity slot
public static Quest Create(QuestStarted e) => new() { QuestKey = e.QuestId };
public void Apply(MembersJoined e) { /* ... */ }
}The source generator matches the identity attribute by simple name, so any attribute named IdentityAttribute works — but JasperFx.IdentityAttribute is the canonical one. As an alternative to a member-level [Identity], you can declare the identity type explicitly at the class level with [AggregateIdentity(typeof(TId))] from JasperFx.Events.Aggregation — useful when the identity is a strong-typed id or isn't a simple property.
For aggregates with required members, the SG's null-snapshot "create-from-default + apply" branch can't new T() directly — required members would leave the compiler complaining. Either provide a static Create(TEvent e) factory (preferred — gives full control) or use the default! init-pattern Marten's guide documents:
public partial class Account
{
public Guid Id { get; set; }
public required string Owner { get; init; }
// Preferred: explicit factory — SG calls this, no default! required.
public static Account Create(AccountOpened e) => new()
{
Id = e.AccountId,
Owner = e.Owner,
};
public void Apply(Deposited e) { /* ... */ }
}If no Create factory is supplied and the SG falls back to the create-from-default path, the emitted code is var s = new Account { Owner = default! }; Apply(e, s); — semantically valid, but the default! hands the aggregate over to your Apply methods in a partially-initialized state. The factory route is cleaner.
Surfaces unaffected by the partial requirement
FlatTableProjection— has its own dictionary-keyed dispatch viaProject<TEvent>(...)/Delete<TEvent>(...), doesn't go through JasperFx.Events apply-method discovery. Nopartialrequired.EfCoreSingleStreamProjection<TDoc, TDbContext>/EfCoreEventProjection<TDbContext>(fromPolecat.EntityFrameworkCore) — overrideDetermineActionAsyncdirectly, also bypass the SG-required path.
Inline-lambda projection registration APIs removed
JasperFx/jasperfx#276 / #286 removed the inline-lambda registration overloads on EventProjection, IAggregationSteps<T, TQuerySession>, and JasperFxAggregationProjectionBase — the source generator cannot dispatch a runtime closure, so these were the last thing keeping FEC reachable. Migrate to conventional method declarations on the projection class:
// before (Polecat 3.x)
public class MyEventProjection : EventProjection
{
public MyEventProjection()
{
Project<OrderPlaced>((e, ops) =>
{
ops.Store(new OrderSummary { Id = e.OrderId, ... });
});
}
}
// after (Polecat 4.0)
public partial class MyEventProjection : EventProjection
{
public void Project(OrderPlaced e, IDocumentSession ops)
{
ops.Store(new OrderSummary { Id = e.OrderId, ... });
}
}The same shape — public void Project(TEvent, IDocumentSession) for inline mutation, plus the conventional Create / Apply / ShouldDelete methods for aggregations — covers everything the lambda APIs used to do. DeleteEvent<TEvent>() (no-args, populates the internal delete-types list) and TransformsEvent<TEvent>() remain available; only the lambda-taking overloads were dropped.
The migration recipe is identical between Polecat and Marten — see Marten's migration guide — "Inline-lambda projection registration APIs removed" for the same content + a longer worked example.
IInlineProjection.ApplyAsync parameter widening
JasperFx.Events 2.0 widened IInlineProjection<TOperations>.ApplyAsync(...) so the streams parameter is IEnumerable<StreamAction> instead of IReadOnlyList<StreamAction> (jasperfx-events#4306). Polecat's built-in projection types (NaturalKeyProjection, FlatTableProjection) were updated to match; third-party IInlineProjection<TOperations> implementors must update their parameter type:
// before
public Task ApplyAsync(TOperations operations,
IReadOnlyList<StreamAction> streams,
CancellationToken cancellation) { ... }
// after
public Task ApplyAsync(TOperations operations,
IEnumerable<StreamAction> streams,
CancellationToken cancellation) { ... }If your implementation relied on streams.Count or indexed access, materialize the parameter once with .ToList() at the top of the method — the widening is intentional to give callers room to stream without forcing materialization.
IJasperFxAggregateGrouper.Group parameter tightening
JasperFx.Events 2.0 promoted the events parameter on IJasperFxAggregateGrouper<TId, TQuerySession>.Group(...) from IEnumerable<IEvent> to IReadOnlyList<IEvent> (jasperfx#201). Polecat does not ship any IAggregateGrouper implementations of its own, so most Polecat users see no impact. If your application has custom groupers, update the parameter type — no logic change is required, and you can drop any defensive events.ToList() you'd been doing at the top of Group:
// before
public Task Group(IQuerySession session,
IEnumerable<IEvent> events,
IEventGrouping<TId> grouping) { ... }
// after
public Task Group(IQuerySession session,
IReadOnlyList<IEvent> events,
IEventGrouping<TId> grouping) { ... }New behavior worth flagging
BulkInsertWithVersionAsync — optimistic concurrency on bulk inserts
#48 / #62. The new BulkInsertMode.OverwriteIfVersionMatches value (added by the Weasel.Core relocation) is exposed through a dedicated sibling method on AdvancedOperations rather than the existing BulkInsertAsync surface — the version-check needs per-row expected versions that the versionless overload has no way to thread:
var batch = new[]
{
(new User { Id = userId, FirstName = "Updated" }, expectedVersion: 1L),
(new User { Id = newUserId, FirstName = "Fresh" }, expectedVersion: 0L),
};
await store.Advanced.BulkInsertWithVersionAsync(batch);Semantics:
- Row exists + stored version == expected version → UPDATE, version bumped to
target.version + 1. - Row does not exist → INSERT at version 1; the expected version is irrelevant on inserts.
- Row exists + stored version != expected version → MERGE is a no-op; after the batch's reader drains,
JasperFx.ConcurrencyException(typeof(T), id)is thrown for the first mismatched id. Matches the per-row pattern used byUpdateOperation/UpsertOperation.
Calling the versionless BulkInsertAsync(docs, BulkInsertMode.OverwriteIfVersionMatches) overload now throws InvalidOperationException pointing callers at BulkInsertWithVersionAsync rather than the Polecat-3.x-era NotSupportedException stub.
WARNING
The concurrency throw is best-effort, not transactional. Matched-and-updated rows in the batch commit before the throw fires; the exception is the signal rather than a rollback. If you need transactional semantics, wrap the call in an outer transaction (TransactionScope or your own BEGIN TRAN / ROLLBACK flow).
FlatTableProjection column casing on SQL Server
#49 (Weasel side: 99e40f0 fix(SqlServer): preserve user casing on TableColumn names, shipped as Weasel 9.0.0-alpha.2). TableColumn names declared on a FlatTableProjection now preserve the casing you wrote in code; pre-fix the projection was silently lower-casing column names, breaking case-sensitive collations. No user action required — the projection schema is regenerated on next migration.
ILongVersioned — 64-bit document revisions
#136 / #138 (consumes jasperfx#348). Polecat already supported JasperFx.IRevisioned (an int Version optimistic-concurrency counter). 4.0 adds the new JasperFx.ILongVersioned (long Version) for documents whose version is a global event sequence number that can exceed Int32.MaxValue — typically MultiStreamProjection-derived views. Implement ILongVersioned instead of IRevisioned on those document types; the store, concurrency check, and read-back all flow the 64-bit value end to end. IRevisioned (int) keeps working unchanged for ordinary per-document revision counters.
This required a one-time schema change — see below.
StoreOptions.SchemaResolver — table-name diagnostics surface
#133 (implements the lifted JasperFx.Events.IDocumentSchemaResolver). A new StoreOptions.SchemaResolver resolves the database table backing any document, projection, or event-store table — qualified ([schema].[table]) or bare. Useful for schema inspection, diagnostics, and projection-coordinator activity tags:
var resolver = store.Options.SchemaResolver;
resolver.For<Customer>(); // "[dbo].[pc_doc_customer]"
resolver.ForEvents(qualified: false); // "pc_events"(Named SchemaResolver rather than Schema because StoreOptions.Schema is already Polecat's SchemaConfiguration.)
Projection-emitted side-effect messages
#84 / #98 / #100. Aggregation projections can now publish side-effect messages through an IMessageOutbox / IMessageBatch, flushed in the same transaction as the projection's storage writes. This is the foundation for Wolverine integration where a projection raises follow-on messages. Inline projections gained the same side-effect support in #99 / #101.
Hot-cold async-daemon coordination
#96, refined through the rc wave (#117–#120, #126, #141). The async daemon now coordinates across nodes: a ProjectionCoordinator (built on the lifted JasperFx.Events.Daemon.ProjectionCoordinatorBase) negotiates per-shard / per-database leadership via SQL Server sp_getapplock (through Weasel.SqlServer.AdvisoryLock), so multiple instances of your app run each projection on exactly one node with automatic failover. No configuration change is required for single-node deployments; multi-node deployments get safe hot-cold handover for free. When the daemon is disabled (DaemonMode.Disabled, the default), no coordinator distributor is built at all.
Schema changes
Unlike the 3.x → early-4.0 transition (which touched no table shapes), the ILongVersioned work introduced one DDL change:
- Revision column widened
int→bigint. #136. Theversioncolumn on document tables (pc_doc_*) is nowbigintto hold 64-bitILongVersionedvalues.IRevisioned(int) reads downcast transparently. The change ships as a non-destructive widening migration — existingintcolumns areALTERed in place (no drop/recreate, no data loss) on the next schema migration. Run your normalApplyAllConfiguredChangesToDatabaseAsync()/ migration step after upgrading; no manual SQL is required.
AOT / codegen posture
Polecat 4 inherits the same AOT-friendly posture introduced in JasperFx 2.0 (jasperfx#213 AOT pillar, jasperfx#190 ITypeLoader abstraction). Polecat itself has been source-generator-first since 3.x — there is no Roslyn runtime-compile path to disable — so:
PublishAot=trueis the supported posture for Polecat 4 applications, modulo the usual System.Text.JsonJsonSerializerContextsetup for your document and event types.IsAotCompatible=trueis now set on the Polecat assembly (PR #67), and the reflective surfaces of Polecat have been progressively annotated for the trimmer / AOT analyzer — Serialization (#74), ProjectionReplay (#75), LINQ extension/provider surface (#76), Storage / Registry / EventStoreExplorer (#77), and the class-level → call-site refactor of the remaining reflective surface (#107).- FastExpressionCompiler is no longer pulled in transitively through
JasperFx.Events(jasperfx#276). Projection apply-method dispatch routes exclusively through[GeneratedEvolver]outputs fromJasperFx.Events.SourceGenerator— see the "Projections and self-aggregating documents must bepartial" section above for the consumer-side migration. - An AOT smoke-test consumer (Polecat.AotSmoke, PR #106) ships with the repo and runs in CI with
WarningsAsErrorscoveringIL2026 / IL2046 / IL2055 / IL2065 / IL2067 / IL2070 / IL2072 / IL2075 / IL2090 / IL2091 / IL2111 / IL3050 / IL3051. Regressions in Polecat's AOT-clean surface fail the build.
Publishing AOT
Polecat 4 applications target the same Critter Stack 2026 AOT pre-gen flow Marten 9 uses — Polecat is a thin consumer of JasperFx.Events, so the consumer-side recipe (csproj flags, WarningsAsErrors=IL* set, source-generator-backed ISerializer swap, projection-class partial requirement, smoke-test pattern) lives in JasperFx's docs rather than being forked here:
- Publishing AOT with JasperFx — the end-to-end Critter Stack guide. Read this first.
- Marten's AOT publishing walkthrough — longer worked example with full csproj + program snippets. Polecat-applicable apart from the Marten-specific service registrations.
Polecat-specific call-outs:
Polecat.AotSmoke(source, originally PR #106) ships in-tree as the canonical AOT consumer example. The csproj setsIsAotCompatible=true+TrimMode=fulland promotes IL2026 / IL2046 / IL2055 / IL2065 / IL2067 / IL2070 / IL2072 / IL2075 / IL2090 / IL2091 / IL2111 / IL3050 / IL3051 to errors — any regression in Polecat's AOT-clean surface fails the build.IsAotCompatible=trueis set onPolecat.csprojitself, plus both shipping extension packages —Polecat.AspNetCoreandPolecat.EntityFrameworkCore(mirrors Marten PR #4468's per-extension audit). Both flagged carry method-level annotations that name the AOT-hostile upstream APIs they wrap:Polecat.AspNetCore—StreamMany<T>/StreamOne<T>/StreamAggregate<T>ExecuteAsyncoverrides carry[RequiresDynamicCode]+[RequiresUnreferencedCode]for theJsonSerializer.SerializeToUtf8Bytes<T>reflective overloads;McpEndpointExtensionsclass-level RUC/RDC for the JSON-RPC payload serialization inMapPolecatMcp. AOT consumers building Minimal-API endpoints can returnnew StreamMany<MyDoc>(query)from a[RequiresDynamicCode]-annotated endpoint, or supply aJsonSerializerContextand write a customIResult.Polecat.EntityFrameworkCore—TDoctype parameters carry the full EF Core entity DAM set (PublicConstructors | NonPublicConstructors | PublicFields | NonPublicFields | PublicProperties | NonPublicProperties | Interfaces) matchingDbContext.Find<TEntity>requirements;TDbContextparameters carryPublicConstructorsmatchingActivator.CreateInstance(typeof(TDbContext), ...). Concrete closed generics (EfCoreSingleStreamProjection<MyEntity, MyDbContext>) satisfy these implicitly.
Polecat.AotSmokealso references both extensions and exercises a constructor /typeof()from each, so a future annotation regression in either extension trips the smoke build before downstream consumers see it.- The post-#276 fail-fast semantics apply identically to AOT consumers — a projection registered without a
[GeneratedEvolver]dispatcher throwsInvalidProjectionExceptionat host build (not at runtime via FEC fallback like Polecat 3.x). The remediation is the same as the non-AOT story — see the SG-only projection apply dispatch section above.
Out of scope (no Polecat 4 change)
- Obsolete API removals. A grep of the 3.x codebase for
[Obsolete]declarations returned no matches; the 4.0 release does not remove any APIs that were obsoleted in 3.x. - Schema migrations. The only
pc_*table-shape change in the whole 3.x → 4.0 line is the documentversioncolumn widening tobigint— covered in Schema changes above, applied non-destructively by the normal migration step. Everything else (event store, streams, tags, hilo, snapshots) is unchanged; a 3.x database upgrades in place.
Dependency lockstep
Polecat 4 ships in lockstep with the rest of the Critter Stack 2026 wave. The supported pairings are:
| Polecat | Marten | JasperFx | JasperFx.Events | Weasel |
|---|---|---|---|---|
| 4.0 | 9.0 | 2.0 | 2.0 | 9.0 |
Mixing major versions across products is unsupported in this wave (the dedup work moves abstractions between assemblies and ABI-binds them to specific majors). If you upgrade Polecat to 4.0, plan to upgrade Marten and Weasel-side dependencies to their 9.0 lines at the same time.

JasperFx provides formal support for Polecat and other Critter Stack libraries. Please check our