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:
-
Initial default is correct — StructuralPropertyConfiguration constructor calls EdmLibHelpers.IsNullable(), which correctly returns true for Nullable<T> value types.
-
Convention overrides it — NullableAttributeEdmPropertyConvention.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
Description
ODataConventionModelBuilderincorrectly marksNullable<T>value type properties (e.g.DateOnly?,int?,TimeOnly?) asNullable="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
NullableAttributeEdmPropertyConventionmisinterprets the C# compiler's[Nullable]attribute for value types:Initial default is correct —
StructuralPropertyConfigurationconstructor callsEdmLibHelpers.IsNullable(), which correctly returnstrueforNullable<T>value types.Convention overrides it —
NullableAttributeEdmPropertyConvention.Apply()reads the[Nullable]attribute emitted by the C# compiler. ForNullable<DateOnly>in a#nullable enablecontext, the compiler emitsNullableAttributewith flag1(non-nullable), because value type nullability is expressed throughNullable<T>in the CLR type system, not through NRT annotations. The convention interprets flag1as "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
Expected metadata
Actual metadata
Impact
When ASP.NET Core OData registers input formatters, the non-nullable EDM property causes the OData deserializer to reject
nullor missing values forDateOnly?properties in POST/PUT/PATCH requests, even though the CLR type explicitly allows null.Workaround
Use
OnModelCreatingto globally fix allNullable<T>value type properties after conventions run:Suggested Fix
In
NullableAttributeEdmPropertyConvention.Apply(), skip properties whosePropertyInfo.PropertyTypeis aNullable<T>value type, since their nullability is already correctly determined byEdmLibHelpers.IsNullable()from the CLR type system.Environment
#nullable enablecontext