Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 29 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1308,10 +1308,11 @@ CoreCLR and throws `missing native code` in a Native AOT image, on the first wri
`AddSubClassHierarchy()` and `RegisterValueType(Type)`. Under the JIT they work as before. Under
Native AOT they throw `NotSupportedException` naming the generic call to use, instead of
"missing native code".
- ⚠️ **`ValueTypeDelegates` is a local workaround for jasperfx#942.** JasperFx's
`ValueTypeInfo.CreateWrapper`/`UnWrapper` compile with FastExpressionCompiler unconditionally,
which throws in a native image. The workaround falls back to reflection invoke there and is
exactly the JasperFx call under the JIT. Delete it when #942 ships.
- **`ValueTypeDelegates` is gone** (fisher#423). It was a local copy of the reflection fallback
jasperfx#942 put into `ValueTypeInfo.CreateWrapper`/`UnWrapper` in JasperFx 2.80.0, and
`StrongTypedIdentification` now calls those directly. The smoke is what shows it: pinned back to
JasperFx 2.79.2 with the workaround deleted, the native run fails at `ValueTypeInfo.CreateWrapper`
from `StrongTypedIdentification`'s constructor, and on 2.80.1 it passes. Do not reintroduce it.
- `strong_typed_identities_declared` re-runs every strong-typed id test through the declared path,
and asserts the factory was captured. Otherwise a capture that silently stopped would leave those
tests passing on the reflective path.
Expand Down Expand Up @@ -1351,11 +1352,20 @@ reloads an inline `Snapshot<T>`.
`Event<>` closing works natively only because every event type is a reference type, served by
Native AOT's shared canonical instantiation. Its method-level suppression says so, and a
value-type event would not be covered.
- ⚠️ **A strong-typed aggregate id cannot work natively until jasperfx#942 ships.** Closing the
projection statically does not help: `JasperFxSingleStreamProjectionBase`'s constructor compiles the
wrapper's accessors with FastExpressionCompiler, which throws there. That was measured, with a
`Snapshot<T, TId>` overload built and then removed because it could not achieve its purpose. The
factory refuses by name, naming #942. It is the one item of fisher#412 still open.
- ⚠️ **A strong-typed aggregate id cannot work natively until jasperfx#950 ships.** Closing the
projection statically does not help: `JasperFxSingleStreamProjectionBase`'s constructor builds its
identity sources with `IEvent.CreateAggregateIdentitySource<TId>()` and
`StreamAction.CreateAggregateIdentitySource<TId>()`, which `CompileFast()` unconditionally for any
wrapper. That was measured twice, with a `Snapshot<T, TId>` overload built and then removed both
times: once against #942 (#398), and again on JasperFx 2.80.1 after #942 shipped (fisher#423).
#942 fixed `ValueTypeInfo`, which is what strong-typed *document* ids needed, and not these two.
The factory refuses by name, naming #950, and the smoke asserts that refusal natively. It is the one
item of fisher#412 still open.
- **The static-closing half is built and parked** on the local branch
`wip-412-strong-typed-aggregates`: `Projections.Snapshot<T, TId>()`, a composite twin and
`Projections.LiveStreamAggregation<T, TId>()`, recording a factory on the event graph that live
aggregation, `Snapshot<T>` and composites all consult. When #950 ships it is that branch plus
removing the refusal.
- **The smoke consumer references `JasperFx.Events.SourceGenerator` itself**, because a project
reference does not carry the analyzer the package bundles. Its version is kept in step by hand.

Expand Down Expand Up @@ -1409,7 +1419,7 @@ compares a `DateOnly`, and runs both projection step-through methods — all nat
instead. Do not inline it back. The remaining `MakeGenericMethod` is inherent (the interface leaves
`TState` unconstrained, or names it only as a `Type`). It carries a method-level suppression, and the
smoke runs it natively.
- **What #412 did not touch:** strong-typed aggregate ids (jasperfx#942, above), and the IL warnings
- **What #412 did not touch:** strong-typed aggregate ids (jasperfx#950, above), and the IL warnings
outside the five paths the issue named. `dotnet build` still lists about twenty against Fisher
(`MessagePublishing`, `AdvancedSqlResultReader`, `SecondaryStoreProxy`, `EnumMember` and others).
ILC reports some of them as reachable from the smoke, and none of them failed when it ran.
Expand Down Expand Up @@ -3080,6 +3090,15 @@ whether the marker is itself the opt-in or merely supplies the member to guard o
dropping the loop leaves all five facts passing and nothing announcing that the config was ignored.
Verified by dropping it.

⚠️ **The mapped half of the suite (jasperfx#943, JasperFx 2.80.0) skips unless the fixture opts in, and
Fisher's did not until fisher#423.** Five facts reach the guard through a member the configuration names
(`MapVersionTo<T>(memberName)`) rather than through `IVersioned`, gated on
`SupportsMappedConcurrencyMember`, which defaults false. So the bump left them *skipped*, not red, over a
route Fisher had supported since fisher#245 — the silent direction. The fixture now replays
`MappedVersionMembers` onto `MappingFor(type).Metadata.Version.MapTo(member)`, the same `MetadataColumn`
the DSL reaches, and flips the flag. Unlike the marker replay above, **this one is load-bearing on
Fisher**: dropping it fails 4 of the 5 facts. Count skipped facts on a compliance bump, not only red ones.

**`UpdateExpectedVersion<T>(T, Guid)` is still absent**, where Marten has it as the Guid counterpart of
`UpdateRevision`. Not needed for the above — seeding off the document covers the ordinary flow — and
not added here; it is the separate case of guarding against a version the caller knows and the document
Expand Down
28 changes: 22 additions & 6 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,23 @@
<!-- Core Critter Stack dependencies. JasperFx and Weasel move in lockstep: Weasel 9.23.x
carries a JasperFx 2.37.x floor, so bump both together rather than letting one resolve
transitively. -->
<!-- JasperFx 2.79.2 (from 2.79.1). One change, jasperfx#940: the source generator keeps the
<!-- JasperFx 2.80.1 (from 2.79.2, spanning 2.80.0). fisher#423. No compile break and no new
suite; two existing suites widened:

- jasperfx#942/#946: ValueTypeInfo.CreateWrapper / UnWrapper fall back to reflection
where they cannot compile. Fisher's local copy of that fallback (ValueTypeDelegates) is
deleted, and strong-typed document ids call ValueTypeInfo directly. NOT the whole of
a strong-typed AGGREGATE id in a native image: JasperFxSingleStreamProjectionBase's
identity sources still CompileFast unconditionally, filed as jasperfx#950.
- jasperfx#943: GuidOptimisticConcurrencyCompliance gained five facts over a mapped
version member. They skip unless the fixture opts in, so they were skipped on the bump;
the fixture now replays MappedVersionMembers and sets SupportsMappedConcurrencyMember.
- jasperfx#944: DocumentSearchCompliance gained four hierarchy facts, green on the bump
alone, since fisher#285 put Fisher's search through Query<T>()'s doc_type filter.
- jasperfx#948 (2.80.1): ResilientEventLoader now really wraps a load failure in
EventLoaderException. Nothing in Fisher matches on that type.

JasperFx 2.79.2 (from 2.79.1). One change, jasperfx#940: the source generator keeps the
evolver constructors it emits under Native AOT, so ILC no longer trims a conventional
aggregate's generated dispatcher away. No suite changed and no test count moved; this is
the floor fisher#398's event-store AOT work builds on.
Expand Down Expand Up @@ -177,16 +193,16 @@
generation, which Fisher does not do — projection dispatch is source-generated.
jasperfx#880 is XML comments on AutoCreate. jasperfx#867 is a Wolverine codegen fix and
jasperfx#865/#866 are the publish pipeline. -->
<PackageVersion Include="JasperFx" Version="2.79.2" />
<PackageVersion Include="JasperFx.Events" Version="2.79.2" />
<PackageVersion Include="JasperFx" Version="2.80.1" />
<PackageVersion Include="JasperFx.Events" Version="2.80.1" />
<!-- The shared cross-store event sourcing compliance suites. Source-only package: the
suites compile inside Fisher.Tests so JasperFx's aggregate source generator binds
Fisher's own session types. See src/Fisher.Tests/Compliance. -->
<PackageVersion Include="JasperFx.Events.ComplianceTests" Version="2.79.2" />
<PackageVersion Include="JasperFx.Events.SourceGenerator" Version="2.79.2" />
<PackageVersion Include="JasperFx.Events.ComplianceTests" Version="2.80.1" />
<PackageVersion Include="JasperFx.Events.SourceGenerator" Version="2.80.1" />
<!-- Fisher.Tests only: the Microsoft.Extensions.AI adapter to IEmbeddingProvider, so the
vector projection page's provider-wiring sample compiles. Fisher itself never takes it. -->
<PackageVersion Include="JasperFx.Events.MicrosoftExtensionsAI" Version="2.79.2" />
<PackageVersion Include="JasperFx.Events.MicrosoftExtensionsAI" Version="2.80.1" />

<!-- Weasel.Sqlite: schema management, table definitions, migrations, PRAGMA-applying data
source. Weasel.Storage: the dialect-neutral closed-shape document + event storage
Expand Down
40 changes: 30 additions & 10 deletions HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ equivalent for and never will.
[CLAUDE.md](CLAUDE.md) has the architecture and the SQLite traps. This document is the compliance
scoreboard and the things that are true right now but not obvious from either.

**2580 tests green on net9.0 and net10.0** — 2513 in `Fisher.Tests`, 36 in
`Fisher.AspNetCore.Tests` and 31 in `Fisher.EntityFrameworkCore.Tests`. 669 of
them are shared cross-store compliance tests — 509 event sourcing and 160 document.
On JasperFx **2.79.2** / Weasel **9.40.0**.
**2589 tests green on net9.0 and net10.0** — 2522 in `Fisher.Tests`, 36 in
`Fisher.AspNetCore.Tests` and 31 in `Fisher.EntityFrameworkCore.Tests`. 678 of
them are shared cross-store compliance tests — 509 event sourcing and 169 document.
On JasperFx **2.80.1** / Weasel **9.40.0**.

## The Weasel 9.40.0 bump — a builder's parameter count

Expand All @@ -28,6 +28,26 @@ default would have sent a caller sizing a value list down a conservative path fo
foreign keys (weasel#677/#681), a `TableDelta` for a table that does not exist answering instead of
throwing (weasel#658), and the migration lock released when an apply fails (weasel#659).

## The JasperFx 2.80.1 bump — jasperfx#942's workaround goes, and five skipped facts run

**No compile break and no new suite; two document suites widened, by nine facts.** fisher#423.

- **jasperfx#942 — `ValueTypeInfo.CreateWrapper`/`UnWrapper` fall back to reflection in a native
image.** Fisher's local copy of that fallback is deleted. `smoke/aot-consumer` shows it is the
upstream one now doing the work: pinned back to 2.79.2 with the copy deleted, the native run fails at
`ValueTypeInfo.CreateWrapper`.
- ⚠️ **A strong-typed *aggregate* id still cannot run natively** — jasperfx#950, filed from this bump.
`JasperFxSingleStreamProjectionBase`'s identity sources `CompileFast()` unconditionally. Fisher keeps
refusing it by name, now naming #950, and the smoke asserts the refusal. The one open item of #412.
- ⚠️ **jasperfx#943 — five `GuidOptimisticConcurrencyCompliance` facts over a mapped version member
were SKIPPED on the bump, not red.** They gate on `SupportsMappedConcurrencyMember`, default false.
Fisher has the route (fisher#245), so the fixture now replays `MappedVersionMembers` and opts in; all
five pass, and dropping the replay fails four.
- **jasperfx#944 — four `DocumentSearchCompliance` hierarchy facts**, green on the bump alone: Fisher's
search has gone through `Query<T>()`'s `doc_type` filter since fisher#285.
- jasperfx#948 (2.80.1) — `ResilientEventLoader` really wraps a load failure in `EventLoaderException`
now. Nothing in Fisher matches on that type, and the dead-letter and skip suites are unchanged.

## The JasperFx 2.79.2 bump — the generated evolver survives Native AOT

**One upstream change, jasperfx#940, and no suite or count moved.** The source generator now keeps the
Expand Down Expand Up @@ -845,8 +865,8 @@ Three of the seven turned up a real defect or a wrong premise, which is the usef

## Where we are against the compliance suites

`JasperFx.Events.ComplianceTests` 2.79.2 ships 59 suites; Fisher enrolls **58 of them, 669 tests**.
Fisher passes **669 of them, across all
`JasperFx.Events.ComplianceTests` 2.80.1 ships 59 suites; Fisher enrolls **58 of them, 678 tests**.
Fisher passes **678 of them, across all
58 suites**. Every suite compiles; every one is also subclassed and running. The five that did not
pass on the 2.65.0 pin were the upstream ones described at the top of this file, and 2.66.0 closed
all five.
Expand Down Expand Up @@ -995,7 +1015,7 @@ naming.
**Green on all fifty-eight is not the same as feature-complete.** The suites cover what is portable
across stores; "Deliberate gaps" below is still the honest list of what Fisher does not do.

### Green — 58 suites, 669 tests
### Green — 58 suites, 678 tests

Event sourcing — 46 suites, 509 tests:

Expand Down Expand Up @@ -1048,22 +1068,22 @@ Event sourcing — 46 suites, 509 tests:
| `EventProjectionRegistrationCompliance` | 3 |
| `AutoDiscoveredAggregateCompliance` | 2 |

Documents — 12 suites, 160 tests, through `FisherDocumentComplianceFixture`:
Documents — 12 suites, 169 tests, through `FisherDocumentComplianceFixture`:

| Suite | Tests |
|---|---|
| `DocumentStoreDiagnosticsCompliance` | 52 |
| `DocumentQueryCompliance` | 17 |
| `DocumentConjoinedTenancyCompliance` | 10 |
| `DocumentSearchCompliance` | 11 |
| `DocumentSearchCompliance` | 15 |
| `DocumentLoadAndStoreCompliance` | 15 |
| `DocumentCommitListenerCompliance` | 10 |
| `DocumentDeleteCompliance` | 10 |
| `NumericRevisionCompliance` | 9 |
| `PendingStreamActionsCompliance` | 9 |
| `DocumentSessionCompliance` | 7 |
| `DocumentSessionEventsCompliance` | 5 |
| `GuidOptimisticConcurrencyCompliance` | 5 |
| `GuidOptimisticConcurrencyCompliance` | 10 |

### Nothing in the fixture throws any more

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ process**. There is no server to install, nothing to provision, and nothing to k
> database-per-tenant, with tenants that appear at runtime), `AddFisher(...)` DI registration, and the
> `Fisher.AspNetCore` and `Fisher.EntityFrameworkCore` companion packages all work and are tested.
>
> Fisher passes **all 58 suites and 669 tests** it enrolls from `JasperFx.Events.ComplianceTests`,
> the shared cross-store suite Marten and Polecat also enroll in, alongside its own 2,513.
> Fisher passes **all 58 suites and 678 tests** it enrolls from `JasperFx.Events.ComplianceTests`,
> the shared cross-store suite Marten and Polecat also enroll in, alongside its own 2,522.
>
> That suite pins **API portability, not behavioural equivalence** — code written against one store
> compiles and runs against another. It does not pin that the three behave identically, and they do
Expand Down
5 changes: 3 additions & 2 deletions docs/configuration/native-aot.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,10 @@ aggregate's `Apply`/`Create` dispatch is source-generated, so nothing about it n
project that references Fisher as a package gets the generator with it.

An aggregate whose identity is a **strong-typed id** does not work in a native image yet. JasperFx
compiles the wrapper's accessors with FastExpressionCompiler, which throws there
([jasperfx#942](https://github.com/JasperFx/jasperfx/issues/942)). Fisher refuses such an aggregate by
compiles a wrapper-keyed aggregate's identity sources with FastExpressionCompiler, which throws there
([jasperfx#950](https://github.com/JasperFx/jasperfx/issues/950)). Fisher refuses such an aggregate by
name rather than failing inside JasperFx. Key it on a Guid, string, `int` or `long` until that ships.
A strong-typed *document* id works, declared as described above.

## Async projections and the daemon

Expand Down
2 changes: 1 addition & 1 deletion smoke/aot-consumer/AotConsumer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<!-- fisher#398. A project reference does not carry the analyzer the Fisher package bundles, so
the generator is referenced here as an application referencing JasperFx directly would.
Kept in step with Directory.Packages.props by hand. -->
<PackageReference Include="JasperFx.Events.SourceGenerator" Version="2.79.2" PrivateAssets="all" />
<PackageReference Include="JasperFx.Events.SourceGenerator" Version="2.80.1" PrivateAssets="all" />
</ItemGroup>

</Project>
35 changes: 35 additions & 0 deletions smoke/aot-consumer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,27 @@
Expect(snapshot is { Port: "Tromso", Legs: 3 }, "the inline snapshot was written and reloads");
}

// ---- a strong-typed aggregate id is refused by name (fisher#423) ----

// JasperFx 2.80.0 fixed ValueTypeInfo's wrappers (jasperfx#942), which is what the strong-typed
// document ids above need. Its single-stream projection base still compiles a wrapper-keyed
// aggregate's identity sources with FastExpressionCompiler (jasperfx#950), so Fisher refuses the
// aggregate by name rather than letting it fail inside JasperFx.
try
{
await using var refused = DocumentStore.For(options =>
{
options.Connection($"Data Source={path}");
options.ConfigureSerialization(configure: json => json.TypeInfoResolver = SmokeJson.Default);
options.Projections.Snapshot<Pod>(SnapshotLifecycle.Inline);
});

throw new Exception("Expected: a strong-typed aggregate id is refused in a native image");
}
catch (NotSupportedException e) when (e.Message.Contains("jasperfx#950"))
{
}

// ---- the async daemon (fisher#412) ----

var ledger = Guid.NewGuid();
Expand Down Expand Up @@ -348,6 +369,18 @@ public void Apply(Arrived arrived)
}
}

public readonly record struct PodId(Guid Value);

public record PeaAdded;

public class Pod
{
public PodId Id { get; set; }
public int Peas { get; set; }

public void Apply(PeaAdded _) => Peas++;
}

public record Deposited(decimal Amount);

public record Withdrawn(decimal Amount);
Expand Down Expand Up @@ -406,6 +439,8 @@ public class Note
public string Body { get; set; } = "";
}

[JsonSerializable(typeof(Pod))]
[JsonSerializable(typeof(PeaAdded))]
[JsonSerializable(typeof(Deposited))]
[JsonSerializable(typeof(Withdrawn))]
[JsonSerializable(typeof(Ledger))]
Expand Down
Loading
Loading