Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ section is the GitHub release's notes.

- Packages: the public surface is checked against 2.3.1's, so nothing 2.3.1 ships is removed or changed in 2.4.0.
- Analyzers: CMTK0005 now also warns at `CollectionsMarshal.SetCount(ids, count)` on a `List<OrderId>`, which makes room for `count` items without putting one there, as `new OrderId[count]` does; a constant count of zero isn't reported. In a build that treats warnings as errors, that can report code that built before ([CMTK0005](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0005)).
- Analyzers: CMTK0009 now warns at `LeftJoin`, `RightJoin` and .NET 11's `FullJoin` whose side with no match is a value object, `Result` or `[RequireCustomInitialization]` struct, which the join fills with `default`; the message names an `Option` of each element, or a nullable projection in a query. In a build that treats warnings as errors, that can report code that built before ([CMTK0009](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Analyzers/README.md#cmtk0009)).

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion design/analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ exist.
| CMTK0006 | Field or auto-property of a no-default struct type in a class that no initializer, `required` or constructor sets; a non-public parameterless constructor (EF, serializers) and a record's copy constructor are exempt. Measured 2026-09-28 on the repository's EF entities and request types: 12 of 12 reports on settable properties of a type with only the implicit constructor, each a real way to a default. `required`, the fix the message names where it compiles, works with EF's compiled model, precompiled queries and Native AOT (the smoke test rerun with it). A `[SetsRequiredMembers]` constructor must assign the `required` members it promises | Warning | shipped in 1.0.0 |
| CMTK0007 | `FromKnownGood` given a value straight from a parameter of the enclosing method or lambda, or a member or element of one. A value the code produced itself is legitimate and silent; a parameter copied into a local first is not seen. Info, since test theories and helpers that only receive constants are reported too | Info | shipped in 1.0.0 |
| CMTK0008 | The wrapped values of two different value objects compared (`==`, `!=`, ordering, `Equals`, `CompareTo`) through `.Value`, `?.Value`, `GetValue()` or `ValueOrNull()`: the typed ids unwrapped into the bug they prevent. Recognised by syntax, since `.Value` of a same-project value object does not bind where analyzers run. Also `string.Equals`, `object.Equals`, a comparer's `Equals`, `string.Compare`/`CompareOrdinal` over two such values (the forms MA0006 rewrites `==` into), and two value objects of different types through `Equals(object)`, which is always false. Also a LINQ join on two such values: a query's `join … on a.Value equals b.Value`, with or without `into`, and `Join`/`GroupJoin`/`LeftJoin`/`RightJoin` of `Enumerable`, `Queryable` and `AsyncEnumerable` whose key selectors are lambdas returning them, resolved by symbol, with the candidates where a key selector does not bind; a composite key is not looked into. A raw value on one side, or an explicit cast, is silent | Warning | shipped in 1.0.0 |
| CMTK0009 | A call that returns the `default` of a no-default struct when it finds nothing: `Enumerable`/`Queryable` `FirstOrDefault`, `LastOrDefault`, `SingleOrDefault`, `ElementAtOrDefault`, `DefaultIfEmpty` without a default value, `Nullable<T>.GetValueOrDefault()`, `GetValueOrDefault(dictionary, key)`, `Activator.CreateInstance`, `RuntimeHelpers.GetUninitializedObject`, and `Option.OrDefault()`. The message names `FirstOrNone`, `GetValueOrNone` or `Or`. Warning, since a sequence checked for emptiness first is correct. Also `ImmutableArray`'s own, `Find`/`FindLast` of `List<T>`, `Array` and `ImmutableList<T>`, `ImmutableDictionary.GetValueOrDefault` and the immutable builders', and the async `FirstOrDefaultAsync`, `LastOrDefaultAsync`, `SingleOrDefaultAsync` and `ElementAtOrDefaultAsync` of `System.Linq.AsyncEnumerable` and EF Core, whose type is the method's type argument; EF Core's only where the project references it. From 2.3.0, with a message of its own, `FirstOrNone`, `SingleOrNone`, `LastOrNone`, `FirstOrError` and `LastOrError` on a receiver that is an `IQueryable<T>` (an implementer or a type parameter constrained to one included) bound to the `IEnumerable<T>` overload because the condition is a delegate: a `Func` variable, field or property, a method group, an anonymous method, a call that returns one. A receiver typed as a sequence, `AsEnumerable()` or a cast, is silent; no code fix, since the condition's rewrite is the author's (#129) | Warning | shipped in 1.0.0 |
| CMTK0009 | A call that returns the `default` of a no-default struct when it finds nothing: `Enumerable`/`Queryable` `FirstOrDefault`, `LastOrDefault`, `SingleOrDefault`, `ElementAtOrDefault`, `DefaultIfEmpty` without a default value, `Nullable<T>.GetValueOrDefault()`, `GetValueOrDefault(dictionary, key)`, `Activator.CreateInstance`, `RuntimeHelpers.GetUninitializedObject`, and `Option.OrDefault()`. The message names `FirstOrNone`, `GetValueOrNone` or `Or`. Warning, since a sequence checked for emptiness first is correct. Also `ImmutableArray`'s own, `Find`/`FindLast` of `List<T>`, `Array` and `ImmutableList<T>`, `ImmutableDictionary.GetValueOrDefault` and the immutable builders', and the async `FirstOrDefaultAsync`, `LastOrDefaultAsync`, `SingleOrDefaultAsync` and `ElementAtOrDefaultAsync` of `System.Linq.AsyncEnumerable` and EF Core, whose type is the method's type argument; EF Core's only where the project references it. From 2.3.0, with a message of its own, `FirstOrNone`, `SingleOrNone`, `LastOrNone`, `FirstOrError` and `LastOrError` on a receiver that is an `IQueryable<T>` (an implementer or a type parameter constrained to one included) bound to the `IEnumerable<T>` overload because the condition is a delegate: a `Func` variable, field or property, a method group, an anonymous method, a call that returns one. A receiver typed as a sequence, `AsEnumerable()` or a cast, is silent; no code fix, since the condition's rewrite is the author's (#129). From 2.4.0 (#261), the side of an outer join with no match: every overload of `LeftJoin` whose inner element, `RightJoin` whose outer element, and .NET 11's `FullJoin` where either element is a no-default struct, a diagnostic per side, on `Enumerable`, `Queryable` and `AsyncEnumerable`, the tuple-returning overloads included, resolved by symbol through `LinqJoins`, which CMTK0008 shares. Where a lambda reads a generated member and the join does not bind, the elements are read from the sequences passed, and the candidate is the one whose outer sequence the receiver is, `Queryable`'s for a query. The message names an `Option` of each element in memory and a nullable projection in a query (EF Core 11.0.0-rc.3 translates it under `FULL JOIN`, KB-059). An `Option` element of a join is not reported: its `default` is `None`, the way out the message names, which reporting it would flag in turn. Warning, no code fix | Warning | shipped in 1.0.0 |
| CMTK0010 | A type argument that breaks a constraint attribute on its parameter (`[Abstract]`, `[Sealed]`, `[Record]`, `[Concrete]`; from 2.1.0 also `[Interface]`, `[Unsealed]`, `[ValueObject]`, `[ClosedSet]`, `[FlagsEnum]`, `[ReadOnlyStruct]`, [decision 81](adr/0081-six-more-kinds-of-type-argument.md), the two marker ones proved by a bound to the marker, the other four by the attribute alone; an interface is no value object, [decision 85](adr/0085-an-interface-is-no-value-object.md); a proof reads what a closed use can be, [decision 84](adr/0084-cmtk0010-proofs-read-what-a-closed-use-can-be.md)), or a pair of arguments for two parameters marked `[Distinct]` or `[Unrelated]` (2.1.0, [decision 79](adr/0079-distinct-and-unrelated-type-parameters.md)) that are one symbol (nullable annotations aside, `dynamic` as `object`) or related by an implicit identity, reference or boxing conversion either way, reported once at the later argument, a forwarding pair a breach unless both carry the attribute in one generic or their C# constraints admit no common (or no related) type, at any closed use: a base type, an implemented interface, a member type, a local, a `typeof`, a call with its type arguments written (at the argument) or inferred (at the call), a method group whose arguments the delegate infers (at the group). A type parameter forwarded without the same attribute is a breach too, unless its C# constraints leave it no breaking argument: `struct`/`unmanaged` or a bound proving `[Sealed]` proves `[Sealed]`; `new()` or whatever proves `[Sealed]` proves `[Concrete]`; a bound that is or proves a record proves `[Record]` (CS8865); nothing but the attribute proves `[Abstract]`. `[Concrete]` accepts an enum, a delegate and `dynamic`, as `new()` and `struct` do for an enum. A default interface method reached through a derived interface carries its own constraint once: it was its own implementation there, and the rule recursed until the compiler's stack overflowed (an independent review, 2026-10-14). Both settled 2026-10-13 before the tag, measured as false reports (an enum, a delegate, `dynamic`; six forwarding forms) with no miss among their neighbours. The open generic in its own body and an unbound `typeof` are silent. Reflection is out of reach | Error | built 2026-10-13 for 2.0.0 (phase 13), a major, so Error is within [decision 34](adr/0034-errors-fixed-within-major-version-warnings.md) |
| CMTK0011 | A concrete class implementing or deriving from a type marked `[RequireParameterlessConstructor]`, directly or through another type, without a parameterless constructor of the required accessibility; a class declaring no constructor satisfies `Public`. A struct is never reported (decided 2026-10-13, before the tag): its parameterless constructor, implicit or declared, is public and can be nothing else (CS8958), so it satisfies `Public` as it is, and under any other accessibility the rule reported an Error only a suppression could answer, and the fix wrote a constructor that did not compile. A wrong accessibility is reported at the constructor. The code fix, the package's first, in `CodoMetis.TypeKit.Analyzers.CodeFixes.dll` beside the analyzer, writes or changes the constructor; a type with a primary constructor in any of its parts gets none (CS8862), nor, since 2026-10-14, one whose implicit `base()` binds to nothing (CS7036, CS0122, CS0121), which a review found the fix writing. It writes its own trivia and asks no formatter, which laid out the whole type again and wrote the machine's line endings: doc comments, attributes, comments and the members' layout stay, added lines take the members' indentation and the brace's line ending, a one-line type gets the constructor on its line | Error | built 2026-10-13 for 2.0.0 (phase 13) |
| CMTK0012 | A closed set's generated `Match` given an argument by position: every parameter is a `Func<TResult>` in declaration order, so reordering two members swapped their branches and compiled. Bound, only the generated signature counts; unbound (the declaring project, before weaving), a call named `Match` on a closed set with one argument per member, its names the generators' rule (the member's name, first letter lower-cased, a keyword escaped). The code fix names every argument after the parameter it binds to now | Error | built 2026-10-13 for 2.0.0 (phase 13), after a review found the swap |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
verified: 2026-10-31
pins:
- external Microsoft.NETCore.App.Ref 11.0.0-rc.3.26522.148 and Microsoft.EntityFrameworkCore.Sqlite 11.0.0-rc.3.26522.148
source: the join surface compiled against the 11.0.0-rc.3 reference assemblies by OuterJoinDefaultTests, and reflected over on the 11.0.0-rc.1 runtime; the translation measured with EF Core 11.0.0-rc.3 on SQLite in a `net11.0` console built by the 11.0.100-rc.1 SDK and run on the 11.0.0-rc.1 runtime, the only ones installed (`ToQueryString` and the rows read back); dotnet/runtime#127236 and "What's new in .NET 11 libraries"
turned-on: CMTK0009's outer joins (#261), which overloads it resolves, and the nullable projection its message and the Analyzers README name for a query
test: none, the repository's SDK cannot target net11.0 to run EF Core 11; the join surface is held by OuterJoinDefaultTests, which compiles against the 11.0.0-rc.3 reference assemblies
---

# KB-059. EF Core 11 translates a nullable projection under `FULL JOIN`

- **.NET 11 adds `FullJoin` on `Enumerable`, `Queryable` and `AsyncEnumerable`.** With a result selector,
`FullJoin<TOuter, TInner, TKey, TResult>(outer, inner, outerKeySelector, innerKeySelector, resultSelector,
comparer = null)`; `AsyncEnumerable` also has the form whose selectors take a `CancellationToken` and return a
`ValueTask`. The type parameters are `TOuter` and `TInner` and the sequences `outer` and `inner`, as in .NET 10's
`LeftJoin` and `RightJoin`.
- **.NET 11 also adds outer joins that return tuples.** `LeftJoin`, `RightJoin` and `FullJoin<TOuter, TInner, TKey>`
take no result selector and return `(TOuter Outer, TInner Inner)`, on all three hosts, with an optional comparer.
The side with no match is `default` in the tuple as it is in the result selector.
- **EF Core 11 translates `FullJoin` to `FULL JOIN`, and a nullable projection with it.** Over a struct column with a
value converter, `db.Lefts.Select(l => (Code?)l.Code).FullJoin(db.Rights.Select(r => (Code?)r.Code), l => l, r => r,
(l, r) => new { l, r })` is `SELECT "l"."Code" AS "l", "r"."Code" AS "r" FROM "Lefts" AS "l" FULL JOIN "Rights" AS
"r" ON "l"."Code" = "r"."Code"`, and over the lefts 10, 20 and the rights 20, 30 reads back `10 | null`,
`20 | 20`, `null | 30`. The same projection under `LeftJoin` is a `LEFT JOIN` with `10 | null`, `20 | 20`.
- **Without the projection, EF throws for the side with no match.** The same `FullJoin` over `l.Code` and `r.Code`
translates to the same SQL and throws `InvalidOperationException: Nullable object must have a value.` when it reads
the rows. LINQ to objects gives `default` instead, which is what CMTK0009 reports.

Checked on SQLite alone; SQL Server and PostgreSQL were not run.
1 change: 1 addition & 0 deletions design/knowledge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,5 @@ test and later for the harness's `Baseline.Fact`.
| [KB-055](KB-055-ericksonlopez-domainprimitives.md) | EricksonLopez.DomainPrimitives 3.0.0 is an attribute on a partial record struct, with a catalogue of ready-made types, re-validated on every read | EricksonLopez.DomainPrimitives 3.0.0 | prose |
| [KB-057](KB-057-a-nullable-contract-has-an-element-type-and-reads-null-as-null.md) | A `Nullable<T>` contract has `T` as its element type, and a JSON null read into it is null whatever `T`'s converter does | OpenApi 10.0.12, SDK 10.0.4xx | `KB057` |
| [KB-058](KB-058-ef-orders-captured-values-in-sql-only-with-a-lambda.md) | EF orders captured values in SQL only when the chain takes a lambda: `values.Max()` runs in memory, `values.Max(v => v)` in SQL | EF Sqlite 10.0.12 | `KB058` |
| [KB-059](KB-059-ef-core-11-translates-a-nullable-projection-under-full-join.md) | .NET 11 adds `FullJoin` and tuple-returning outer joins on all three LINQ hosts, and EF Core 11 translates a nullable projection under `FULL JOIN`, where a non-nullable one throws for the side with no match | .NET 11.0.0-rc.3, EF Sqlite 11.0.0-rc.3 | prose |
| [KB-060](KB-060-metalama-2026-1-on-the-net-11-sdk.md) | Metalama 2026.1 weaves a `net11.0` project on SDK 11 RC3 at C# 14, and a web project fails with `LAMA0625` when SDK 11 is the only SDK | .NET SDK 11 RC3, Metalama 2026.1.30, 2027.0.5-preview | prose |
Loading
Loading