We’re thrilled to announce the release of ASP.NET Core OData 10.0.0 Preview 1, a major modernization update that embraces .NET’s native System.DateOnly and System.TimeOnly types! This release upgrades to .NET 10.0 and replaces OData’s proprietary Edm.Date and Edm.TimeOfDay CLR wrapper types with .NET’s native structs.
What’s Changed
Framework & Dependency Updates
.NET 10.0 Support
ASP.NET Core OData now targets .NET 10.0, taking advantage of the latest runtime performance improvements and language features.
OData Library 9.x and Model Builder 3.x Integration
This release updates OData dependencies to align with the latest OData .NET ecosystem:
- OData Library (ODL):
9.0.0-preview.2→9.0.0-preview.3 - OData Model Builder:
2.0.0→3.0.0-preview.1
This coordinated release ensures seamless integration between ASP.NET Core OData and OData Model Builder, providing a unified and consistent experience when building EDM models with modern .NET types.
Native .NET Type Support
The most significant change is the migration from legacy EDM date/timeOfDay wrapper types to .NET’s native date and time types:
What’s Changed:
- ❌ Removed: Support for
Microsoft.OData.Edm.DateandMicrosoft.OData.Edm.TimeOfDay - ✅ Added: Native support for
System.DateOnlyandSystem.TimeOnly
Important Note: While the CLR types have been changed to System.DateOnly and System.TimeOnly, the CSDL metadata continues to use Edm.Date and Edm.TimeOfDay primitive type kinds for OData protocol compatibility. Future releases will address complete replacement in the metadata layer.
Why This Matters
Alignment with Modern .NET
By adopting .NET 10.0 and native DateOnly/TimeOnly types, ASP.NET Core OData now leverages capabilities that are:
- Native to .NET: Eliminates the need for custom EDM-specific wrapper types
- Consistent: Same types used across .NET applications, and ASP.NET Core
- Performant: Optimized by the .NET runtime as value types, benefiting from .NET 10.0’s performance improvements
OData Serialization Format Compliance
While DateOnly and TimeOnly do not directly align with the OData v4.01 specification’s Edm.Date and Edm.TimeOfDay type definitions (which are defined as distinct EDM primitive types), ASP.NET Core OData provides serialization and deserialization to ensure wire-format compatibility:
- DateOnly serializes to OData Date format:
yyyy-MM-dd(e.g.,2025-11-27) - TimeOnly serializes to OData TimeOfDay format:
HH:mm:ss.fffffff(e.g.,14:30:45.1234567)
Developer Note: The default .ToString() methods produce culture-dependent output (MM/dd/yyyy for DateOnly and short time format for TimeOnly). The OData serializers handle correct formatting, but if you need to manually format these types, use the ToODataString() extension methods to ensure compliance with ABNF construction rules.
Breaking Changes
⚠️ This is a breaking change for applications using Microsoft.OData.Edm.Date or Microsoft.OData.Edm.TimeOfDay.
To upgrade to ASP.NET Core OData 10.0.0 Preview 1, you will need to:
- Upgrade to .NET 10.0: Update your project’s target framework to
net10.0 - Update package dependencies:
Microsoft.AspNetCore.OData→10.0.0-preview.1Microsoft.OData.ModelBuilder→3.0.0-preview.1
- Replace types in your models:
Date→DateOnlyTimeOfDay→TimeOnly
- Update models to use native types
Migration Guide
Before (ASP.NET Core OData 9.x)
using Microsoft.OData.Edm;
public class Event
{
public int Id { get; set; }
public string Title { get; set; }
public Date EventDate { get; set; }
public TimeOfDay StartTime { get; set; }
public TimeOfDay EndTime { get; set; }
}
After (ASP.NET Core OData 10.x)
using System;
public class Event
{
public int Id { get; set; }
public string Title { get; set; }
public DateOnly EventDate { get; set; }
public TimeOnly StartTime { get; set; }
public TimeOnly EndTime { get; set; }
}
Model Configuration
All model configurations using Date or TimeOfDay properties must be updated to use DateOnly and TimeOnly. The good news is that the API surface remains otherwise identical—simply replace the types and you’re ready to go!
Getting Started
Prerequisites
Ensure you have the .NET 10.0 SDK installed:
dotnet --version
# Should output 10.0.x or higher
Installation
Install the preview packages via the .NET CLI:
dotnet add package Microsoft.AspNetCore.OData --version 10.0.0-preview.1
Or update your .csproj file directly:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OData" Version="10.0.0-preview.1" />
</ItemGroup>
</Project>
Complete Example
Here’s a comprehensive example demonstrating DateOnly and TimeOnly support in ASP.NET Core OData:
Step 1: Define Your Entity Model
using System;
using System.Collections.Generic;
namespace ODataDateTimeSample.Models;
public class Event
{
public int Id { get; set; }
public string Title { get; set; }
public string Location { get; set; }
// Non-nullable DateOnly and TimeOnly
public DateOnly EventDate { get; set; }
public TimeOnly StartTime { get; set; }
public TimeOnly EndTime { get; set; }
// Nullable DateOnly and TimeOnly
public DateOnly? RegistrationDeadline { get; set; }
public TimeOnly? DoorOpenTime { get; set; }
// Collections of DateOnly and TimeOnly
public IList<DateOnly> AlternateDates { get; set; }
public IList<TimeOnly> SessionTimes { get; set; }
}
Step 2: Build the EDM Model
Convention-Based Model
using Microsoft.AspNetCore.OData; using Microsoft.OData.ModelBuilder; var builder = WebApplication.CreateBuilder(args); // Build the EDM model using conventions var modelBuilder = new ODataConventionModelBuilder(); modelBuilder.EntitySet<Event>("Events"); var edmModel = modelBuilder.GetEdmModel(); // Add OData services with query features builder.Services.AddControllers() .AddOData(options => options .EnableQueryFeatures(100) .AddRouteComponents("odata", edmModel)); var app = builder.Build(); app.MapControllers(); app.Run();
Step 3: Create the OData Controller
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using ODataDateTimeSample.Models;
namespace ODataDateTimeSample.Controllers;
public class EventsController : ODataController
{
private static readonly List _events = new()
{
new Event
{
Id = 1,
Title = ".NET Conference 2025",
Location = "Seattle Convention Center",
EventDate = new DateOnly(2025, 11, 15),
StartTime = new TimeOnly(9, 0, 0),
EndTime = new TimeOnly(17, 30, 0),
RegistrationDeadline = new DateOnly(2025, 11, 1),
DoorOpenTime = new TimeOnly(8, 30, 0),
AlternateDates = new List
{
new DateOnly(2025, 11, 16),
new DateOnly(2025, 11, 17)
},
SessionTimes = new List
{
new TimeOnly(10, 0, 0),
new TimeOnly(14, 0, 0),
new TimeOnly(16, 0, 0)
}
},
new Event
{
Id = 2,
Title = "Azure Workshop",
Location = "Online",
EventDate = new DateOnly(2025, 12, 5),
StartTime = new TimeOnly(13, 0, 0),
EndTime = new TimeOnly(16, 0, 0)
}
};
// GET: odata/Events
[EnableQuery]
public IActionResult Get()
{
return Ok(_events);
}
// GET: odata/Events(1)
[EnableQuery]
public IActionResult Get(int key)
{
var evt = _events.FirstOrDefault(e => e.Id == key);
if (evt == null)
{
return NotFound();
}
return Ok(evt);
}
// POST: odata/Events
[HttpPost]
public IActionResult Post([FromBody] Event evt)
{
evt.Id = _events.Max(e => e.Id) + 1;
_events.Add(evt);
return Created(evt);
}
}
Step 4: Add OData Functions with DateOnly/TimeOnly Parameters
// In your model builder
// Define the Bound function to collection
var function = modelBuilder.EntityType()
.Collection
.Function("GetEventsInDateRange")
.ReturnsFromEntitySet("Events");
function.Parameter("startDate");
function.Parameter("endDate");
function.Parameter<TimeOnly?>("preferredTime");
// In your controller
[EnableQuery]
[HttpGet]
public IActionResult GetEventsInDateRange([FromODataUri] DateOnly startDate, [FromODataUri] DateOnly endDate, [FromODataUri] TimeOnly? preferredTime)
{
var filtered = _events.Where(e =>
e.EventDate >= startDate &&
e.EventDate <= endDate); if (preferredTime.HasValue) { filtered = filtered.Where(e =>
e.StartTime <= preferredTime.Value && e.EndTime >= preferredTime.Value);
}
return Ok(filtered);
}
Step 5: Test Your OData API
Once your application is running, test it with these OData queries:
Query all events:
GET /odata/Events
Filter by EventDate:
GET /odata/Events?$filter=EventDate eq 2025-12-05GET /odata/Events?$filter=EventDate ge 2025-11-01GET /odata/Events?$filter=month(EventDate) eq 12
Filter by StartTime:
GET /odata/Events?$filter=StartTime eq 13:00:00GET /odata/Events?$filter=StartTime lt 10:00:00 GET /odata/Events?$filter=minute(EndTime) eq 30
Order by EventDate and StartTime:
GET /odata/Events?$orderby=EventDate desc, StartTime
Add a new Event:
POST /odata/Events
Content-Type: application/json
{
"Id": 23,
"Title": "Test title",
"Location": "Online",
"EventDate": "2023-05-15",
"StartTime": "20:30:59.0010100",
"EndTime": "08:30:59.0010000",
"AlternateDates": ["2023-05-15", "2024-10-24"],
"SessionTimes": ["20:30:59.0010100", "08:30:59.0010000", "21:31:59"]
}
Call a custom function:
GET /odata/Events/GetEventsInDateRange(startDate=2025-11-01,endDate=2025-12-31,preferredTime=10:00:00)
Feedback
This is a preview release, and we want to hear from you! Please try ASP.NET Core OData 10.0.0 Preview 1 in your projects and share your experiences, issues, or suggestions on the GitHub repository.
Your feedback is invaluable as we work toward the final 10.0.0 release and continue to modernize the library.
Related Resources
0 comments
Be the first to start the discussion.