[#4454 Phase 1D] Reflection-driven CompiledQueryHandlerDescriptor fallback#4456
Merged
jeremydmiller merged 2 commits intoMay 18, 2026
Merged
Conversation
…lback When CompiledQueryHandlerRegistry.TryGet(queryType) misses — i.e. the consumer's assembly is missing the [JasperFxAssembly] marker that the source generator needs, or the query type was registered at runtime via reflection — we now build the descriptor reflectively from the freshly walked CompiledQueryPlan instead of falling through to CompiledQuerySourceBuilder / JasperFx.RuntimeCompiler. The constructed descriptor is registered with CompiledQueryHandlerRegistry so subsequent session.Query(compiledQuery) invocations hit the fast registry path — build cost is paid once per query type, never per row. The new RuntimeCompiledQueryDescriptorFactory mirrors what CompiledQuerySourceGenerator emits: * BindParameter — a per-(queryType) dictionary of per-member binders. Each binder knows the NpgsqlDbType + the read strategy for one parameter member (scalar / array / enum / byte[]). EnumStorage branches inline on the enumAsString flag the descriptor caller passes, so the same descriptor serves stores with mixed enum-storage configs. * AttachIncludeReaders — dispatches by the include member's declared type (Action<T>, IList<T> / List<T>, IDictionary<TId, T> / Dictionary<TId, T>) onto Include.ReaderToAction<T> / ReaderToList<T> / ReaderToDictionary<T, TId> via MakeGenericMethod. One MakeGenericMethod call per include member per session.Query(...) — cheap relative to the SQL round-trip that follows. * ReadStatistics — direct MemberInfo.GetValue read against the StatisticsMember captured on the plan, or null when the query has no statistics member. DocType is resolved from the query type's ICompiledQuery<TDoc, TOut> generic arguments via reflection (covers ICompiledListQuery / future shape extensions because they all extend ICompiledQuery<,>). DocumentStore.CompiledQueryCollection's registry-miss path collapses to "build descriptor, register, hand to SourceGeneratedCompiledQuerySource" — a 22-line block replacing the prior CompiledQueryCodeFile + Roslyn emit fallthrough. The codegen path (CompiledQuerySourceBuilder, CompiledQueryCodeFile, StatelessCompiledQuery / ClonedCompiledQuery / ComplexCompiledQuery base classes) is now runtime-unreachable — Phase 1E deletes the dead code. The factory carries [RequiresUnreferencedCode] + [RequiresDynamicCode] so AOT publishers see the right warning at the boundary; #4454 Phase 5's AOT-compliance pass decides whether this fallback survives long-term or gets replaced behind those annotations with a source-gen-only contract. Verification: dotnet test src/CompiledQueryTests/CompiledQueryTests.csproj → 9/9 pass dotnet test src/LinqTests/LinqTests.csproj → 1257/1258 pass (one unrelated skip carried over from master)
The runtime include-reader dispatcher passed the full GetGenericArguments() array to typeof(IList<>).MakeGenericType regardless of arity, which threw "The number of generic arguments provided doesn't equal the arity of the generic type definition" for compiled-query includes shaped as Dictionary<TId, TDoc> (2 args going to a 1-arg generic). Split the dispatch by args.Length first so each MakeGenericType call sees the right shape. Covers LinqTests.Bugs.Bug_4043_include_plus_contains .CompiledQuery_WithIncludeOnly_Works and LinqTests.Includes.end_to_end_query_with_compiled_include_Tests.compiled_include_to_dictionary.
jeremydmiller
added a commit
that referenced
this pull request
May 18, 2026
Phase 1D (#4456) made the JasperFx.RuntimeCompiler-driven path runtime- unreachable for live compiled-query dispatch — every session.Query(...) now goes through CompiledQueryHandlerRegistry (source-gen-emitted descriptor) or RuntimeCompiledQueryDescriptorFactory (reflection fallback) and lands in SourceGeneratedCompiledQuerySource<TOut>. The Roslyn-emit classes and the ICodeFile contract for compiled queries were carrying their weight as a "just in case" PoC bridge; the bridge is gone. Delete it. Files removed: - CompiledQueryCodeFile.cs (ICodeFile producer for the codegen CLI) - CompiledQuerySourceBuilder.cs (Roslyn-emit assembly builder) - CompiledQuerySource.cs (codegen base for emitted sources) - CompiledSourceType.cs (the Stateless/Cloneable/Complex enum) - StatelessCompiledQuery.cs / ClonedCompiledQuery.cs / ComplexCompiledQuery.cs (the three codegen-emitted handler base classes — superseded by SourceGeneratedStatelessHandler / SourceGeneratedClonedHandler / SourceGeneratedComplexHandler) - WriteSerializedJsonParameterFrame.cs (codegen Frame for the containment / JsonPath JSON-payload writers; runtime equivalent is CompiledQueryDictionaryBuilder) Member-level deletions: - ICompiledQueryAwareFilter.GenerateCode — the source-gen + FEC paths consume BuildSetter() now; no codegen consumer remains. All seven filter implementations drop their GenerateCode overrides: · StringContainsFilter, StringStartsWithFilter, StringEndsWithFilter, StringEqualsIgnoreCaseFilter · ContainmentWhereFilter, ChildCollectionJsonPathCountFilter, DictionaryContainsKeyFilter - ParameterUsage.GenerateCode / generateSimpleCode / generateEnumCode / npgsqlArrayDbTypeCodeFor — the runtime descriptor factory owns the same dispatch in RuntimeCompiledQueryDescriptorFactory. - CompiledQueryPlan.GenerateCode — orchestrated the codegen emit over the plan's commands; no caller. - DocumentStore.CompiledQueryCollection's ICodeFileCollection.BuildFiles() compiled-query branch — Phase 5 will retire the `dotnet marten codegen ...` CLI surface and the ICodeFileCollection contract along with it. For now BuildFiles returns Array.Empty so non-compiled-query ICodeFile consumers keep working. The DictionaryValueUsage class survives the WriteSerializedJsonParameterFrame deletion — moved to its own file because the runtime CompiledQueryDictionaryBuilder + filter Apply paths still need it. The codegen-only IDictionaryPart tree (DictionaryDeclaration, ArrayContainer, ArrayDeclaration, ArrayScalarValue, DictionaryValue) had no other consumers and is gone. Verification: dotnet test src/CompiledQueryTests/CompiledQueryTests.csproj → 9/9 pass dotnet test src/LinqTests/LinqTests.csproj --filter "FullyQualifiedName~Compiled" → 75/75 pass 901 lines of dead code removed.
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 1D of #4454. When
CompiledQueryHandlerRegistry.TryGet(queryType)misses — the consumer's assembly is missing the[JasperFxAssembly]marker thatMarten.SourceGeneratorneeds, or the query type was registered at runtime via reflection — we now build the descriptor reflectively from the freshly walkedCompiledQueryPlaninstead of falling through toCompiledQuerySourceBuilder/JasperFx.RuntimeCompiler. The constructed descriptor is registered withCompiledQueryHandlerRegistryso subsequentsession.Query(compiledQuery)invocations hit the fast registry path — descriptor build cost is paid once per query type, never per row.After this PR, the Roslyn-emit codegen path (
CompiledQuerySourceBuilder,CompiledQueryCodeFile,StatelessCompiledQuery/ClonedCompiledQuery/ComplexCompiledQuerybase classes) is runtime-unreachable for the live query dispatch path. Phase 1E deletes the dead code; onlyBuildFiles()'s codegen surface for thedotnet marten codegen ...CLI remains, and Phase 5 retires that.(Phase 1C — "finish the source generator" — was already covered by the in-tree
CompiledQuerySourceGenerator's iteration-2/3 emit. The generator already discoversICompiledQuery<TDoc, TOut>implementations in[JasperFxAssembly]-marked assemblies and emits a[ModuleInitializer]per type that registers aCompiledQueryHandlerDescriptor. This PR closes the runtime side of 1C+1D — the fallback when the generator hasn't run.)What's in
RuntimeCompiledQueryDescriptorFactoryMirrors the source generator's emit:
BindParameter— a per-queryTypedictionary of per-member binders. Each binder knows theNpgsqlDbType+ read strategy for one parameter member (scalar / array / enum /byte[]). EnumStorage branches inline on theenumAsStringflag the descriptor caller passes, so a single descriptor serves stores with mixed enum-storage configs.AttachIncludeReaders— dispatches by the include member's declared type (Action<T>,IList<T>/List<T>,IDictionary<TId, T>/Dictionary<TId, T>) ontoInclude.ReaderToAction<T>/ReaderToList<T>/ReaderToDictionary<T, TId>viaMakeGenericMethod. OneMakeGenericMethodcall per include member persession.Query(...)— cheap relative to the SQL round-trip that follows.ReadStatistics— directMemberInfo.GetValueread against theStatisticsMembercaptured on the plan, or null when the query has no statistics member.DocTypeis resolved from the query type'sICompiledQuery<TDoc, TOut>generic arguments via reflection (coversICompiledListQueryand any future shape extensions because they all extendICompiledQuery<,>).The factory carries
[RequiresUnreferencedCode]+[RequiresDynamicCode]so AOT publishers see the right warning at the boundary. #4454 Phase 5's AOT-compliance pass decides whether this fallback survives long-term or gets replaced behind those annotations with a source-gen-only contract.Call-site collapse
DocumentStore.CompiledQueryCollection's registry-miss path collapses to:22-line block replacing the prior
CompiledQueryCodeFile+ Roslyn-emit fallthrough.Verification
Follow-up phases
CompiledQuerySourceBuilder,CompiledQueryCodeFile, the three codegen-emitted handler base classes, theGenerateCodemethod onICompiledQueryAwareFilter).JasperFx.RuntimeCompilerPackageReference+ retiredotnet marten codegen ...CLI verbs. That's the "Boom" PR.🤖 Generated with Claude Code