- Microsoft.OData.Core 9.0.0 Preview 3
- Microsoft.OData.Edm 9.0.0 Preview 3
- Microsoft.Spatial 9.0.0 Preview 3
- Microsoft.OData.Client 9.0.0 Preview 3
What’s New in OData .NET (ODL) 9 Preview 3
The OData .NET (ODL) 9.0.0 Preview 3 introduces a set of important changes and cleanup efforts to modernize the library, align with .NET’s evolving runtime, and simplify the OData developer experience. Below, we’ll go through the notable changes included in this release.
Use SingleOrDefault() for Action Queries Returning a Single Item
Summary
We updated DataServiceActionQuerySingle<T> to avoid throwing an exception when an action returns no result. The GetValue() and EndGetValue() methods now choose between SingleOrDefault() and Single() based on whether T can accept null. This preserves fail‑fast behavior for non‑nullable types, while returning null for nullable types.
Details
Previously, GetValue()/EndGetValue() always used LINQ’s Single(). When an action returned an empty sequence, this led to an InvalidOperationException, even for scenarios where clients expected a “no result” to map to null. Community feedback flagged this as a long‑standing pain point.
To address this safely, we added a type check that evaluates the generic type parameter T:
- Nullable or reference types: use
SingleOrDefault()– returnsnullwhen no element is found. - Non‑nullable value types: keep
Single()– still throws if no element is found, preserving fail‑fast behavior.
This strikes a balance between runtime safety and predictable semantics across different T. It also aligns action‑query behavior with prior feedback on GetValue* consistency raised in earlier issues.
Migration guidance Most apps won’t need code changes, but review the following:
- Non‑nullable assignments remain fail‑fast
If you previously relied on
InvalidOperationExceptionto detect missing results forint,Guid, or other non‑nullable types, behavior is unchanged – an empty sequence still throws. Keep your existing exception handling. - Nullable/reference targets may now receive
nullIf you assign toint?,string, or other nullable/reference types,GetValue()/EndGetValue()can now returnnullon “no result.” Ensure your code checks fornullrather than expecting an exception. This is the primary behavior change for those type categories. - Asynchronous patterns
If you wrap
BeginGetValue/EndGetValueor useGetValueAsync()pathways, confirm your null checks and exception handling are consistent, particularly in areas where earlier versions showed inconsistency between sync/async flows.
Example
Previous (non‑nullable, expected exception on empty result – unchanged):
// Throws if no element is returned (fail-fast preserved)
var count = new DataServiceActionQuerySingle(context, actionUri).GetValue();
New (nullable, returns null on empty result – handle gracefully):
int? count = new DataServiceActionQuerySingle<int?>(context, actionUri).GetValue();
if (count is null)
{
// handle no-result case
}
Why this matters
The change removes surprising crashes for clients that expect “no result” to mean null (especially for reference/nullable types), while avoiding a silent regression for non‑nullable targets by keeping the fail‑fast safeguard.
Addressed .NET 8 Obsoletion Warnings
Summary
ODL 9 resolves obsoletion warnings in .NET 8 related to legacy serialization APIs. These warnings (SYSLIB0051) occur when using constructors and methods tied to ISerializable.
Details Removed:
- Serialization constructors following
.ctor(SerializationInfo, StreamingContext). - Implicit implementations of
ISerializable.GetObjectData(SerializationInfo, StreamingContext). - Implicit implementations of
IObjectReference.GetRealObject(StreamingContext).
Why this matters This cleanup ensures compatibility with .NET 8+ and eliminates compile-time warnings for consumers building on modern frameworks.
Migration guidance
- No action required for most consumers.
- If you previously relied on these constructors or methods for custom serialization, review your code and adopt recommended .NET patterns for serialization.
IEdmOperation now uses IEdmOperationReturn
Summary
We’ve modernized the EDM API by replacing the ReturnType property on IEdmOperation with an IEdmOperationReturn instance. This change aligns the interface with the design introduced in OData .NET 7.x and completes the transition planned for 9.x.
Details
- Before (7.x): Introduced
IEdmOperationReturnbut keptIEdmOperation.ReturnTypefor compatibility. - 8.x: Marked
ReturnTypeas[Obsolete]and recommended usingGetReturn()extension method. - Now (9.x): Removed
ReturnTypeandGetReturn().IEdmOperationdirectly exposesIEdmOperationReturnvia itsReturnproperty.
The IEdmOperationReturn interface provides:
public interface IEdmOperationReturn : IEdmElement, IEdmVocabularyAnnotatable
{
IEdmTypeReference Type { get; }
IEdmOperation DeclaringOperation { get; }
}
Migration guidance
- Replace any usage of
ReturnTypewithReturn.Type. - Remove calls to
GetReturn(); use the newReturnproperty instead. - If you previously annotated return types via
GetReturn(), apply annotations directly onIEdmOperationReturn.
Example
// Previous:
var returnType = operation.ReturnType;
var returnInfo = operation.GetReturn();
// New:
var returnType = operation.Return.Type;
Support for Key Aliases in Entity Types
Summary OData .NET now supports key aliases in entity type definitions, aligning with the OData CSDL JSON specification. This enables scenarios where key properties reference complex properties or related entity types, improving interoperability with services that use aliases for composite keys.
Details Previously, ODL only supported simple keys defined directly on the entity type. The OData spec (see https://docs.oasis-open.org/odata/odata-csdl-json/v4.01/odata-csdl-json-v4.01.html#sec_KeyOData Spec – Key) requires that when a key property is a path (e.g., a property of a complex type), the key must specify an alias. This alias must be:
- A simple identifier.
- Unique within the declaring entity type and its base types.
- Not defined for primitive properties directly on the entity type.
Example: Key alias in CSDL XML
<EntityType Name="Category">
<Key>
<PropertyRef Name="Info/ID" Alias="EntityInfoID" />
</Key>
<Property Name="Info" Type="Sales.EntityInfo" Nullable="false" />
<Property Name="Name" Type="Edm.String" />
</EntityType>
<ComplexType Name="EntityInfo">
<Property Name="ID" Type="Edm.Int32" Nullable="false" />
<Property Name="Created" Type="Edm.DateTimeOffset" />
</ComplexType>
Why this matters Key aliases are essential for modeling composite keys that involve nested properties. Without alias support, services using complex keys could not be represented accurately in ODL.
Migration guidance
- No breaking changes for existing models using simple keys.
- For models with complex keys, you can now define aliases in CSDL and consume them via ODL.
- Ensure aliases are unique and follow spec rules.
Removed CsdlTarget Support
Summary
We have removed support for CsdlTarget in OData .NET 9.x. This property was originally introduced to differentiate between OData CSDL and Entity Framework (EF) CSDL formats. With ODL now targeting .NET Core and EF Core, this distinction is no longer relevant.
Details
In earlier versions, ODL supported both OData CSDL and EF6 CSDL (used in EDMX files). CsdlTarget allowed consumers to specify which format to generate. EF Core does not use EDMX or CSDL XML files; it relies on internal model graphs. Maintaining CsdlTarget would introduce inconsistency and unnecessary complexity. In summary:
- EF CSDL generation is no longer supported.
- ODL focuses exclusively on OData CSDL for .NET Core scenarios.
Migration guidance
- Remove any references to
CsdlTargetin your code. - If you previously generated EF CSDL, note that EF Core does not support this format. Use EF Core’s built‑in model APIs instead.
IEdmModel.Find* Overloads for ReadOnlySpan<char> — faster, allocation‑free lookups
Summary
We added ReadOnlySpan<char> overloads to common IEdmModel lookup methods (e.g., FindEntitySet, FindDeclaredType, FindDeclaredOperations). This lets callers pass slices of a path without allocating substrings, improving throughput and reducing GC pressure in routing, URI parsing, and CSDL/annotation processing.
Details
Historically, model lookups accepted only string parameters, so callers had to split a path like "Sales/Orders/ByDate" into multiple substrings to look up individual segments. By introducing span‑based overloads — e.g., IEdmModel.FindEntitySet(ReadOnlySpan<char> entitySetName) — callers can slice and probe the original buffer directly (no intermediate string allocations). This aligns with prior internal proposals to adopt ReadOnlySpan<char> across ODL parsing code paths to avoid new string(...) churn.
Example
// Before: substring allocation
var entitySetName = path.Substring(start, length);
var entitySet = model.FindDeclaredEntitySet(entitySetName);
// Now: allocation-free slice
var entitySetName = segmentText.AsSpan(start, length);
var entitySet = model.FindDeclaredEntitySet(entitySetName);
This pattern generalizes to FindDeclaredType, FindDeclaredOperations, and related utilities. See existing string‑only methods in the public API surface for context. https://learn.microsoft.com/dotnet/api/microsoft.odata.edm.iedmmodel?view=odata-edm-7.0
Why this matters
ReadOnlySpan<char> provides a stack‑allocated view over memory, ideal for parsers and routers that frequently slice text. Using spans avoids temporary strings, reduces GC pressure, and typically yields noticeable performance wins in hot paths.
Migration guidance
- No breaking changes: existing
stringoverloads remain. - Optional optimization: where you already compute indices into larger buffers (e.g., OData path segments), switch to
AsSpan(start, length)and call the new overloads to reduce allocations. - Framework note: span APIs are first‑class in modern .NET; if you still target legacy frameworks, span support is limited and may require compatibility packages. ODL 9 targets modern .NET, so most consumers can adopt spans directly.
Untyped Numeric Deserialization
Summary
We improved deserialization for untyped values so that common primitive numerics (e.g., int, long, double, float) and collections are handled correctly. A compatibility flag — ReadUntypedNumericAsDecimal — lets you choose between preserving legacy behaviour (numbers as decimal, e.g., for precision) or inferring more specific numeric CLR types.
Details
Historically, ODL’s untyped value handling only recognized bool and string; all other numerics defaulted to decimal. That led to surprising results such as 42 materializing as System.Decimal rather than System.Int32 for open/dynamic properties. The new implementation expands support to infer numeric types when appropriate and adds a feature flag to control the behavior.
- Default (type inference):
ReadUntypedNumericAsDecimal = falseUntyped numerics are inferred and converted to specific CLR types (int,long,double,float) based on the runtime value, aligning deserialization with caller expectations for well‑known numeric values. - Alternative (precision‑preserving):
ReadUntypedNumericAsDecimal = trueUntyped numerics are preserved asdecimalto avoid precision loss, keeping wire compatibility with existing services that depend on decimal materialization.
Example
var messageReaderSettings = new ODataMessageReaderSettings
{
// Toggle compatibility flag to preserve untyped numerics as decimal
LibraryCompatibility = ODataLibraryCompatibility.ReadUntypedNumericAsDecimal
};
Note: In ambiguous cases without type information (no declared property and no @odata.type), the OData JSON spec describes heuristics for interpreting values. Historical behavior in ODL favored decimal; adopting double would be more spec‑aligned in some scenarios, but could be a silent breaking change. The new flag provides a controlled opt‑in path.
Why this matters
- Fewer surprises for dynamic/open types – well‑known numerics deserialize to expected CLR types.
- Precision‑sensitive workflows can toggle legacy decimal behavior, minimizing compatibility risk.
- The feature‑flag approach avoids wire semantics regressions while enabling forward‑compatible adoption.
Migration guidance
- Evaluate your dynamic/untyped usage: For scenarios expecting
int/long/double, this will be the default behaviour. Verify downstream type checks and JSON serialization remain consistent. - Opt‑in to legacy decimal behaviour if necessary: If you’d like your code to assume
decimalfor untyped numerics (e.g., financial calculations), opt‑in to the behaviour by use ofReadUntypedNumericAsDecimalcompatibility flag.
.NET 10 Compatibility: Avoid System.MemoryExtensions.Contains in LINQ Expression Trees
Summary
We addressed a breaking change introduced in .NET 10 where LINQ expressions involving Contains on arrays now compile to use System.MemoryExtensions.Contains with ReadOnlySpan<T>. This method cannot be translated into OData query expressions because ReadOnlySpan<T> is a ref struct and its implicit conversion introduces stack pointers, causing NotSupportedException during expression tree processing.
Details
- Behavior in .NET 8/9
LINQ
Containscalls compiled toSystem.Linq.Enumerable.Containson an array:.Call System.Linq.Enumerable.Contains( .NewArray System.String[] { "Milk" }, $p.Name) - Behavior in .NET 10
The same query now compiles to:
.Call System.MemoryExtensions.Contains( .Call System.ReadOnlySpan`1[System.String].op_Implicit(.NewArray System.String[] { "Milk" }), $p.Name)This introduces a
ReadOnlySpan<T>conversion, which is unsupported in reflection‑based invocation because the declaring type contains stack pointers. The runtime throws:System.NotSupportedException: Specified method is not supported.
Fix
ODL now detects and avoids translating MemoryExtensions.Contains in expression trees. Instead, it falls back to the safe pattern using Enumerable.Contains for query translation, ensuring compatibility across .NET versions.
Why this matters
Without this fix, OData LINQ queries using Contains would fail under .NET 10 with NotSupportedException. This change restores predictable behavior and prevents runtime crashes when upgrading to .NET 10.
Migration guidance
- No code changes required for most consumers.
- If you implement custom expression visitors or rely on
MemoryExtensions.Containsin query translation, ensure you handle or avoidReadOnlySpan<T>conversions. - For performance‑critical in‑memory filtering, you can still use
MemoryExtensions.Containsoutside of expression trees.
Removal of ReadUntypedAsString and Alignment with Standard Untyped Handling
Summary
ODL 9 removes the ReadUntypedAsString setting from ODataMessageReaderSettings. This setting allowed untyped values to be read as raw JSON strings (ODataUntypedValue), enabling payload patterns that deviate from the OData specification. Starting with ODL 9, untyped values are always materialized as structured types (ODataResource, ODataCollectionValue, ODataPrimitiveValue) based on their actual content. This change enforces spec‑compliant payload structures and eliminates ambiguity in annotation placement.
Why this matters This is not just an API‑breaking change – it can also break payloads that relied on the old behavior. Specifically:
- Previously, annotations for untyped structured properties could appear as property‑level annotations (e.g.,
undeclaredComplex1@my.Annotation1). - Under the new logic, annotations for structured values must appear inside the structured object (e.g.,
"undeclaredComplex1": { "@my.Annotation1": "..." }). - Old payloads using property‑level annotations for structured values will fail to deserialize under ODL 9.
Implications
- Roundtrip changes: Reading and writing untyped structured properties now produces payloads with annotations embedded inside the object, not alongside the property name.
- Validation tightening: Invalid patterns (e.g.,
@odata.typeas a property annotation) will throw exceptions unless corrected. - Removed configuration:
ReadUntypedAsStringis gone; untyped collections and complex values are always parsed into structured representations.
Example
Previous (ODL ≤ 8.x, ReadUntypedAsString=true):
{
"undeclaredComplex1@my.Annotation1": "custom annotation value",
"undeclaredComplex1": {
"MyProp1": "aaaaaaaaa"
}
}
New (ODL 9):
{
"undeclaredComplex1": {
"@my.Annotation1": "custom annotation value",
"MyProp1": "aaaaaaaaa"
}
}
Migration guidance
- Audit payloads If your clients or services emit annotations for structured properties as property‑level annotations, update them to embed annotations inside the structured object.
- Plan for coordination This change affects wire format. Services upgrading to ODL 9 must coordinate with clients to ensure payloads conform to the new structure.
- Versioning strategy Consider endpoint versioning or phased rollout if you cannot synchronize all clients immediately.
Why we made this change
- The old behavior was non‑compliant with the OData spec and introduced inconsistencies.
ReadUntypedAsStringwas rarely used and primarily supported ill‑formed payloads.- Removing this feature simplifies the reader/writer pipeline and ensures predictable, spec‑aligned behavior.
Refactored Transcoding Logic — Use Encoding.CreateTranscodingStream
Summary
ODL 9 replaces the custom TranscodingWriteStream implementation with the built‑in Encoding.CreateTranscodingStream API introduced in .NET 8. This change simplifies the codebase, reduces maintenance overhead, and leverages the optimized framework implementation for encoding conversion.
Details
- Previous behavior
ODL shipped its own
TranscodingWriteStreamto handle scenarios where payloads needed to be written in a different encoding than the underlying stream. - New behavior
The library now uses:
var transcodingStream = Encoding.CreateTranscodingStream(outputStream, sourceEncoding, targetEncoding);This API is part of the .NET standard library and provides a robust, well‑tested solution for transcoding streams.
Why this matters
- Removes custom code that duplicated framework functionality.
- Improves performance and reliability by using the optimized .NET implementation.
- Reduces complexity for future maintenance.
Migration guidance
- No breaking changes for consumers — behavior remains consistent.
Modernized Date and Time Handling — Use System.DateOnly and System.TimeOnly
Summary
ODL 9 replaces legacy EDM primitives Microsoft.OData.Edm.Date and Microsoft.OData.Edm.TimeOfDay with .NET’s built‑in System.DateOnly and System.TimeOnly. This aligns OData .NET with modern .NET standards and improves consistency with framework APIs.
Details
- Internal refactor
All usages of
Edm.DateandEdm.TimeOfDayhave been replaced withDateOnlyandTimeOnly. - Function and property names
Updated to match .NET naming conventions:
hours→hourminutes→minuteseconds→secondmilliseconds→millisecond
- Error handling
Invalid time values now throw
ArgumentOutOfRangeException(fromTimeOnly) instead ofFormatException. Error messages originate from .NET APIs for consistency. - String formatting
Previous:
TimeSpan.ToString(@\"hh\\:mm\\:ss\\.fffffff\", CultureInfo.InvariantCulture)Now:TimeOnly.ToString(@\"HH\\:mm\\:ss\\.fffffff\", CultureInfo.InvariantCulture)
Example
int hour = 20, minute = 30, second = 59, millisecond = 1;
var timeOnly = new TimeOnly(hour, minute, second, millisecond);
Console.WriteLine(timeOnly.ToString(@"HH:mm:ss.fffffff", CultureInfo.InvariantCulture));
// Output: 20:30:59.0010000
Spec alignment
The OData spec requires Date and TimeOfDay values to conform to dateValue and timeOfDayValue rules. DateOnly and TimeOnly follow these rules, ensuring protocol compliance:
- https://docs.oasis-open.org/odata/odata-csdl-json/v4.01/odata-csdl-json-v4.01.html#sec_DateOData Spec – Date
- https://docs.oasis-open.org/odata/odata-csdl-json/v4.01/odata-csdl-json-v4.01.html#sec_TimeofDayOData Spec – TimeOfDay
Migration guidance
- If you reference
Edm.DateorEdm.TimeOfDaydirectly, update your code to useDateOnlyandTimeOnly. - Validate any custom serialization logic to ensure it uses the correct format strings.
- Exception handling may need updates if you previously caught
FormatException.
Summary
ODL 9 Preview 3 focuses on safety, compatibility, and modern .NET alignment with a small set of high‑impact changes:
- Safer defaults: Action queries now return
nullfor nullable/reference types instead of throwing, while preserving fail‑fast behavior for non‑nullable value types. - Spec‑aligned untyped handling: Removed
ReadUntypedAsString; untyped values are always materialized as structured types, and annotations for structured values are embedded inside the object. - Performance & ergonomics: Added
ReadOnlySpan<char>overloads for model lookups and replaced custom transcoding withEncoding.CreateTranscodingStream. - Modern primitives: Switched to
System.DateOnly/System.TimeOnlyfor date/time, and fixed .NET 10 LINQ expression translation to avoid span‑related runtime issues. - Compatibility cleanup: Addressed .NET 8 obsoletion warnings and removed legacy EF CSDL target support; the library now targets modern .NET runtimes.
These updates make ODL 9 more predictable and easier to maintain. Try the preview packages on NuGet and share feedback on the https://github.com/OData/odata.net/issuesGitHub repo.
0 comments
Be the first to start the discussion.