Skip to content

NullableAttributeEdmPropertyConvention incorrectly marks Nullable<T> value type properties as non-nullable #61

@sergiojrdotnet

Description

@sergiojrdotnet

Description

ODataConventionModelBuilder incorrectly marks Nullable<T> value type properties (e.g. DateOnly?, int?, TimeOnly?) as Nullable="false" in the EDM model. This is a regression introduced by the NRT (Non-nullable Reference Types) convention support added in #16 / PR #17.

Root Cause

The NullableAttributeEdmPropertyConvention misinterprets the C# compiler's [Nullable] attribute for value types:

  1. Initial default is correctStructuralPropertyConfiguration constructor calls EdmLibHelpers.IsNullable(), which correctly returns true for Nullable<T> value types.

  2. Convention overrides itNullableAttributeEdmPropertyConvention.Apply() reads the [Nullable] attribute emitted by the C# compiler. For Nullable<DateOnly> in a #nullable enable context, the compiler emits NullableAttribute with flag 1 (non-nullable), because value type nullability is expressed through Nullable<T> in the CLR type system, not through NRT annotations. The convention interprets flag 1 as "make it non-nullable," overriding the correct default.

Reference types (e.g. string?) are not affected because their [Nullable] flags correctly indicate nullability (2 = nullable).

Reproduce

#nullable enable

public sealed record PersonResponse
{
    public required Guid Id { get; init; }
    public required string FirstName { get; init; }
    public DateOnly? DateOfBirth { get; init; }  // <-- incorrectly becomes Nullable="false"
    public string? Email { get; init; }           // <-- correctly remains nullable
}

var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<PersonResponse>("persons");
var model = modelBuilder.GetEdmModel();

Expected metadata

<Property Name="DateOfBirth" Type="Edm.Date" />
<!-- or explicitly: Nullable="true" -->

Actual metadata

<Property Name="DateOfBirth" Type="Edm.Date" Nullable="false" />

Impact

When ASP.NET Core OData registers input formatters, the non-nullable EDM property causes the OData deserializer to reject null or missing values for DateOnly? properties in POST/PUT/PATCH requests, even though the CLR type explicitly allows null.

Workaround

Use OnModelCreating to globally fix all Nullable<T> value type properties after conventions run:

var modelBuilder = new ODataConventionModelBuilder
{
    OnModelCreating = builder =>
    {
        foreach (var type in builder.StructuralTypes)
        {
            foreach (var property in type.Properties)
            {
                if (property is PrimitivePropertyConfiguration primitive
                    && Nullable.GetUnderlyingType(property.PropertyInfo.PropertyType) is not null)
                {
                    primitive.IsNullable();
                }
            }
        }
    },
};

Suggested Fix

In NullableAttributeEdmPropertyConvention.Apply(), skip properties whose PropertyInfo.PropertyType is a Nullable<T> value type, since their nullability is already correctly determined by EdmLibHelpers.IsNullable() from the CLR type system.

Environment

  • Microsoft.OData.ModelBuilder 2.0.0
  • Microsoft.AspNetCore.OData 9.4.1
  • .NET 10 (also reproducible on .NET 8/9)
  • #nullable enable context

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions