diff --git a/entity-framework/core/providers/cosmos/querying.md b/entity-framework/core/providers/cosmos/querying.md index ccf7b92eee..3ca86a582a 100644 --- a/entity-framework/core/providers/cosmos/querying.md +++ b/entity-framework/core/providers/cosmos/querying.md @@ -213,6 +213,51 @@ Note that to filter out documents where a value is missing: + +```csharp +var results = await context.Entities + .Where(x => EF.Functions.IsDefined(x.Associate!.NestedAssociate!.Id)) + .Select(x => new { x.Associate!.NestedAssociate!.Id }) + .ToListAsync(); +``` + +Alternatively, use to substitute a default value for any property that could be `undefined`: + +```csharp +var results = await context.Entities + .Select(x => new { Id = EF.Functions.CoalesceUndefined(x.Associate!.NestedAssociate!.Id, Guid.Empty) }) + .ToListAsync(); +``` + +### Naked projections and SELECT VALUE + +A _naked projection_ — where a single value is projected directly without wrapping it in a DTO or anonymous type — is translated using `SELECT VALUE` in Cosmos DB SQL. As a result, any documents where the projected value is `undefined` are **silently skipped** and not included in the results: + +```csharp +// Naked projection - translated as SELECT VALUE, undefined results are silently omitted +var ids = await context.Entities + .Select(x => x.Associate!.NestedAssociate!.Id) + .ToListAsync(); +``` + +In contrast, any top-level instantiation in the projection (anonymous type, DTO, entity, or complex type) does **not** use `SELECT VALUE`. When a part of such a projection is `undefined`, an `InvalidOperationException` is thrown as described above. + +If silently skipping undefined results is not the desired behavior, wrap the projected value in an anonymous type or DTO to get a consistent error instead: + +```csharp +// Wrapped in an anonymous type - does not use SELECT VALUE, throws if undefined +var results = await context.Entities + .Select(x => new { x.Associate!.NestedAssociate!.Id }) + .ToListAsync(); +``` + ## Function mappings This section shows which .NET methods and members are translated into which SQL functions when querying with the Azure Cosmos DB provider. diff --git a/entity-framework/core/what-is-new/ef-core-11.0/breaking-changes.md b/entity-framework/core/what-is-new/ef-core-11.0/breaking-changes.md index f4e3c56230..ab8de1318f 100644 --- a/entity-framework/core/what-is-new/ef-core-11.0/breaking-changes.md +++ b/entity-framework/core/what-is-new/ef-core-11.0/breaking-changes.md @@ -23,6 +23,7 @@ This page documents API and behavior changes that have the potential to break ex |:--------------------------------------------------------------------------------------------------------------- | -----------| | [Sync I/O via the Azure Cosmos DB provider has been fully removed](#cosmos-nosync) | Medium | | [Microsoft.Data.SqlClient has been updated to 7.0](#sqlclient-7) | Medium | +| [Cosmos: exception thrown when a projection evaluates to undefined](#cosmos-undefined-projection) | Medium | | [Cosmos: illegal `id` characters are no longer escaped](#cosmos-no-id-escape) | Medium | | [SQL Server compatibility level now defaults to 160](#sqlserver-compatibility-level-160) | Low | | [EF Core now throws by default when no migrations are found](#migrations-not-found) | Low | @@ -126,6 +127,65 @@ If your application uses composite keys whose values can contain the characters - **Existing data**: Documents previously stored in Cosmos DB have `id` values using the old escape sequences (e.g. `Post|1|^2F`). After upgrading to EF Core 11, EF will generate unescaped `id` values (e.g. `Post|1|/`) and will no longer find those existing documents. To continue accessing existing data without migration, opt back into the old behavior using the `AppContext` switch described above—however, be aware that the id-collision bug will still be present. - **New data**: If you are creating a new application or database, avoid using these illegal characters in key values, as they are not valid in Cosmos DB resource `id` values. See the [Azure documentation](xref:Microsoft.Azure.Documents.Resource.Id) for details. + + +### Cosmos: exception thrown when a projection evaluates to undefined + +[Tracking Issue #34067](https://github.com/dotnet/efcore/issues/34067) + +#### Old behavior + +Previously, when projecting properties in anonymous type or DTO projections via navigation over optional relationships where a segment of the path was absent in the Cosmos DB document (causing the projected value to be `undefined`), the behavior was inconsistent: + +- With **single-property** anonymous type or DTO projections, EF translated the query using `SELECT VALUE`, which silently filtered out any documents where the projected value was `undefined`. This meant fewer results were returned than expected, with no indication of the missing data. +- With **multi-property** anonymous type or DTO projections, an `InvalidOperationException` with the message "Nullable object must have a value" was thrown. + +For example, given an entity `Entity` with an optional owned `Associate` which in turn has an optional owned `NestedAssociate`: + +```csharp +// Previously silently returned fewer results (undefined results were filtered out) +var singlePropResults = await context.Entities + .Select(x => new { x.Associate!.NestedAssociate!.Id }) + .ToListAsync(); + +// Previously threw InvalidOperationException: Nullable object must have a value +var multiPropResults = await context.Entities + .Select(x => new { x.Associate!.NestedAssociate!.Id, x.Associate!.NestedAssociate!.String }) + .ToListAsync(); +``` + +#### New behavior + +Starting with EF Core 11.0, an `InvalidOperationException` is thrown in both cases when any part of the projection evaluates to `undefined` in Azure Cosmos DB. The exception message is: + +> A part of the projection was undefined, use the coalesce operator to handle possible undefined values. + +#### Why + +The previous behavior was inconsistent. Single-property projections could silently discard results, making it easy to miss data without any indication of the problem. The new behavior ensures consistent, predictable error reporting whenever a projection encounters an undefined value. + +#### Mitigations + +Use to filter out documents where the projected value is missing: + +```csharp +var results = await context.Entities + .Where(x => EF.Functions.IsDefined(x.Associate!.NestedAssociate!.Id)) + .Select(x => new { x.Associate!.NestedAssociate!.Id }) + .ToListAsync(); +``` + +Alternatively, use to provide a default value for properties that could be `undefined`: + +```csharp +var results = await context.Entities + .Select(x => new + { + Id = EF.Functions.CoalesceUndefined(x.Associate!.NestedAssociate!.Id, Guid.Empty) + }) + .ToListAsync(); +``` + ## Low-impact changes diff --git a/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md b/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md index 8aef322ac0..210e20c29d 100644 --- a/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md +++ b/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md @@ -791,6 +791,25 @@ For more information, [see the documentation](xref:core/providers/cosmos/saving# This feature was contributed by [@JoasE](https://github.com/JoasE) - many thanks! + + +### Consistent behavior for undefined values in projections + +Previously, when projecting scalar properties via optional navigations where a document path was absent (resulting in an `undefined` value in Cosmos DB), behavior was inconsistent: single-property anonymous type projections silently dropped those results, while multi-property projections threw a cryptic exception. + +EF Core 11 now consistently throws an `InvalidOperationException` when any part of a projection evaluates to `undefined`. Use to filter or to provide fallbacks: + +```csharp +var results = await context.Entities + .Where(x => EF.Functions.IsDefined(x.Associate!.NestedAssociate!.Id)) + .Select(x => new { x.Associate!.NestedAssociate!.Id }) + .ToListAsync(); +``` + +For more information, see the [breaking changes documentation](xref:core/what-is-new/ef-core-11.0/breaking-changes#cosmos-undefined-projection). + +This feature was contributed by [@JoasE](https://github.com/JoasE) - many thanks! + ## Migrations