- 
                Notifications
    
You must be signed in to change notification settings  - Fork 179
 
Description
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
- Have a navigation property on your data model that is a related entity that is optional (nullable).
 - Either configure the property on the EDM entity type to be automatically expanded, or just add the property to your expand query parameter.
 - 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:
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)