Skip to content

Optional Navigations Broken (Nullable object must have a value) #1035

@andy-clymer

Description

@andy-clymer

Assemblies affected
ASP.NET Core OData 8.2.2
.NET 7
EF Core 7.0.10

Describe the bug
When expanding a navigation property that can be nullable, and the result set returns at least one record that doesn't have a value set for this optional navigation (null), the following error is returned: Nullable object must have a value.

This works in version 8.2.0, but is broken in both the version listed above and 8.2.1.

Reproduce steps

  1. Have a navigation property on your data model that is a related entity that is optional (nullable).
  2. Either configure the property on the EDM entity type to be automatically expanded, or just add the property to your expand query parameter.
  3. Ensure the result set returns at least one record that doesn't have a value set for this optional navigation.

Data Model

public class User
{
    public User(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
    
    public Guid Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public int Age { get; set; }

    // Foreign Keys
    public Guid? VehicleId { get; set; }
    
    // Navigations
    public Vehicle? PrimaryVehicle { get; set; }
}

EDM (CSDL) Model

<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
  <edmx:DataServices>
    <Schema Namespace="ODataSample.Data" xmlns="http://docs.oasis-open.org/odata/ns/edm">
      <EntityType Name="User">
        <Key>
          <PropertyRef Name="id" />
        </Key>
        <Property Name="id" Type="Edm.Guid" Nullable="false" />
        <Property Name="firstName" Type="Edm.String" Nullable="false" />
        <Property Name="lastName" Type="Edm.String" Nullable="false" />
        <Property Name="age" Type="Edm.Int32" Nullable="false" />
        <Property Name="vehicleId" Type="Edm.Guid" />
        <NavigationProperty Name="primaryVehicle" Type="ODataSample.Data.Vehicle">
          <ReferentialConstraint Property="vehicleId" ReferencedProperty="id" />
        </NavigationProperty>
      </EntityType>
      <EntityType Name="Vehicle">
        <Key>
          <PropertyRef Name="id" />
        </Key>
        <Property Name="id" Type="Edm.Guid" Nullable="false" />
        <Property Name="make" Type="Edm.String" Nullable="false" />
        <Property Name="model" Type="Edm.String" Nullable="false" />
        <Property Name="color" Type="Edm.String" Nullable="false" />
      </EntityType>
    </Schema>
    <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
      <EntityContainer Name="Container">
        <EntitySet Name="Users" EntityType="ODataSample.Data.User">
          <NavigationPropertyBinding Path="primaryVehicle" Target="Vehicles" />
        </EntitySet>
        <EntitySet Name="Vehicles" EntityType="ODataSample.Data.Vehicle" />
      </EntityContainer>
    </Schema>
  </edmx:DataServices>
</edmx:Edmx>
public static IEdmModel GetEdmModelV1()
  {
      var builder = new ODataConventionModelBuilder();
      builder.EnableLowerCamelCase();

      // Users
      var userSet = builder.EntitySet<User>("Users");
      var userType = userSet.EntityType;
      userType.HasKey(t => t.Id);
      userType.Property(t => t.FirstName);
      userType.Property(t => t.LastName);
      userType.Property(t => t.Age);
      userType.Property(t => t.VehicleId);
      userType.HasOptional(t => t.PrimaryVehicle);

      // Vehicles
      var vehicleSet = builder.EntitySet<Vehicle>("Vehicles");
      var vehicleType = vehicleSet.EntityType;
      vehicleType.HasKey(t => t.Id);
      vehicleType.Property(t => t.Make);
      vehicleType.Property(t => t.Model);
      vehicleType.Property(t => t.Color);

      return builder.GetEdmModel();
  }

Request/Response
Request Uri:

https://localhost:7012/v1/Users?$expand=primaryVehicle

Response:

Status Code: 500 - Nullable object must have a value.

Expected behavior
Expected behavior is to return a response of the queried records that have the optional navigation set if they contain a value and for records that don't have a value set to also be returned. This would make querying work with optional navigations work like it did in version 8.2.0.

Additional context
System.InvalidOperationException: Nullable object must have a value.
at System.Nullable1.get_Value() at lambda_method18(Closure, QueryContext, DbDataReader, ResultContext, SplitQueryResultCoordinator) at Microsoft.EntityFrameworkCore.Query.Internal.SplitQueryingEnumerable1.AsyncEnumerator.MoveNextAsync()
at System.Text.Json.Serialization.Converters.IAsyncEnumerableOfTConverter2.OnWriteResume(Utf8JsonWriter writer, TAsyncEnumerable value, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.Serialization.JsonCollectionConverter2.OnTryWrite(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonConverter1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.Serialization.JsonConverter1.WriteCore(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonConverter`1.WriteCoreAsObject(Utf8JsonWriter writer, Object value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.JsonSerializer.WriteCore[TValue](Utf8JsonWriter writer, TValue& value, JsonTypeInfo jsonTypeInfo, WriteStack& state)
at System.Text.Json.JsonSerializer.WriteStreamAsync[TValue](Stream utf8Json, TValue value, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken)
at System.Text.Json.JsonSerializer.WriteStreamAsync[TValue](Stream utf8Json, TValue value, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken)
at System.Text.Json.JsonSerializer.WriteStreamAsync[TValue](Stream utf8Json, TValue value, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|30_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|28_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions