NullableAttributeEdmPropertyConvention incorrectly marks Nullable<T> value type properties as non-nullable
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 22
- Forks
- 24
- PR merge metrics
- No merged PRs in 30d
Description
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 —
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
#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 enablecontext
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at NullableAttributeEdmPropertyConvention.Apply() and inspect how it handles PropertyInfo.PropertyType for Nullable value types. Reproduce the DateOnly? case with ODataConventionModelBuilder, then verify the generated EDM keeps the property nullable and that nullable reference type handling remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- api
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100