{"id":5963,"date":"2025-11-21T13:33:23","date_gmt":"2025-11-21T20:33:23","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/odata\/?p=5963"},"modified":"2025-11-22T13:58:46","modified_gmt":"2025-11-22T20:58:46","slug":"announcing-odata-net-odl-9-preview-3-release","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/odata\/announcing-odata-net-odl-9-preview-3-release\/","title":{"rendered":"Announcing OData .NET (ODL) 9 Preview 3 Release"},"content":{"rendered":"<article>We\u2019re happy to announce that OData .NET (ODL) 9 Preview 3 has been officially released and is available on NuGet:<\/p>\n<ul>\n<li><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.OData.Core\/9.0.0-preview.3\">Microsoft.OData.Core 9.0.0 Preview 3<\/a><\/li>\n<li><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.OData.Edm\/9.0.0-preview.3\">Microsoft.OData.Edm 9.0.0 Preview 3<\/a><\/li>\n<li><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.Spatial\/9.0.0-preview.3\">Microsoft.Spatial 9.0.0 Preview 3<\/a><\/li>\n<li><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.OData.Client\/9.0.0-preview.3\">Microsoft.OData.Client 9.0.0 Preview 3<\/a><\/li>\n<\/ul>\n<h2>What\u2019s New in OData .NET (ODL) 9 Preview 3<\/h2>\n<p>The OData .NET (ODL) 9.0.0 Preview 3 introduces a set of important changes and cleanup efforts to modernize the library,\nalign with .NET\u2019s evolving runtime, and simplify the OData developer experience. Below, we\u2019ll go through the notable\nchanges included in this release.<\/p>\n<h3>Use <code>SingleOrDefault()<\/code> for Action Queries Returning a Single Item<\/h3>\n<p><strong>Summary<\/strong><\/p>\n<p>We updated <code>DataServiceActionQuerySingle&lt;T&gt;<\/code> to avoid throwing an exception when an action returns no result. The <code>GetValue()<\/code> and <code>EndGetValue()<\/code> methods now choose between <code>SingleOrDefault()<\/code> and <code>Single()<\/code> based on whether <code>T<\/code> can accept <code>null<\/code>. This preserves fail\u2011fast behavior for non\u2011nullable types, while returning <code>null<\/code> for nullable types.<\/p>\n<p><strong>Details<\/strong><\/p>\n<p>Previously, <code>GetValue()<\/code>\/<code>EndGetValue()<\/code> always used LINQ\u2019s <code>Single()<\/code>. When an action returned an empty sequence, this led to an <code>InvalidOperationException<\/code>, even for scenarios where clients expected a \u201cno result\u201d to map to <code>null<\/code>. Community feedback flagged this as a long\u2011standing pain point.<\/p>\n<p>To address this safely, we added a type check that evaluates the generic type parameter <code>T<\/code>:<\/p>\n<ul>\n<li><strong>Nullable or reference types<\/strong>: Use <code>SingleOrDefault()<\/code> \u2013 returns <code>null<\/code> when no element is found.<\/li>\n<li><strong>Non\u2011nullable value types<\/strong>: Keep <code>Single()<\/code> \u2013 still throws if no element is found, preserving fail\u2011fast behavior.<\/li>\n<\/ul>\n<p>This strikes a balance between runtime safety and predictable semantics across different <code>T<\/code>. It also aligns action\u2011query behavior with prior feedback on <code>GetValue*<\/code> consistency raised in earlier issues.<\/p>\n<p><strong>Migration guidance<\/strong><\/p>\n<p>Most apps won\u2019t need code changes, but review the following:<\/p>\n<ol>\n<li><strong>Non\u2011nullable assignments remain fail\u2011fast<\/strong>: If you previously relied on <code>InvalidOperationException<\/code> to detect missing results for <code>int<\/code>, <code>Guid<\/code>, or other non\u2011nullable types, behavior is unchanged \u2013 an empty sequence still throws. Keep your existing exception handling.<\/li>\n<li><strong>Nullable\/reference targets may now receive <code>null<\/code><\/strong>: If you assign to <code>int?<\/code>, <code>string<\/code>, or other nullable\/reference types, <code>GetValue()<\/code>\/<code>EndGetValue()<\/code> can now return <code>null<\/code> on \u201cno result.\u201d Ensure your code checks for <code>null<\/code> rather than expecting an exception. This is the primary behavior change for those type categories.<\/li>\n<li><strong>Asynchronous patterns<\/strong>: If you wrap <code>BeginGetValue<\/code>\/<code>EndGetValue<\/code> or use <code>GetValueAsync()<\/code> pathways, confirm your null checks and exception handling are consistent, particularly in areas where earlier versions showed inconsistency between sync\/async flows.<\/li>\n<\/ol>\n<p><strong>Example<\/strong><\/p>\n<p><em>Previous (non\u2011nullable, expected exception on empty result \u2013 unchanged):<\/em><\/p>\n<pre class=\"prettyprint language-cs language-csharp\"><code class=\"language-cs language-csharp\">\/\/ Throws if no element is returned (fail-fast preserved)\r\nvar count = new DataServiceActionQuerySingle(context, actionUri).GetValue();<\/code><\/pre>\n<p><em>New (nullable, returns null on empty result \u2013 handle gracefully):<\/em><\/p>\n<pre class=\"prettyprint language-cs language-csharp\"><code class=\"language-cs language-csharp\">int? count = new DataServiceActionQuerySingle&lt;int?&gt;(context, actionUri).GetValue();\r\nif (count is null)\r\n{\r\n    \/\/ handle no-result case\r\n}<\/code><\/pre>\n<p><strong>Why this matters<\/strong><\/p>\n<p>The change removes surprising crashes for clients that expect \u201cno result\u201d to mean <code>null<\/code> (especially for reference\/nullable types), while avoiding a silent regression for non\u2011nullable targets by keeping the fail\u2011fast safeguard.<\/p>\n<hr \/>\n<h3>Addressed .NET 8 Obsoletion Warnings<\/h3>\n<p><strong>Summary<\/strong><\/p>\n<p>ODL 9 resolves obsoletion warnings in .NET 8 related to legacy serialization APIs. These warnings (SYSLIB0051) occur when using constructors and methods tied to <code>ISerializable<\/code>.<\/p>\n<p><strong>Details<\/strong><\/p>\n<p>Removed:<\/p>\n<ul>\n<li>Serialization constructors following <code>.ctor(SerializationInfo, StreamingContext)<\/code>.<\/li>\n<li>Implicit implementations of <code>ISerializable.GetObjectData(SerializationInfo, StreamingContext)<\/code>.<\/li>\n<li>Implicit implementations of <code>IObjectReference.GetRealObject(StreamingContext)<\/code>.<\/li>\n<\/ul>\n<p><strong>Why this matters<\/strong><\/p>\n<p>This cleanup ensures compatibility with .NET 8+ and eliminates compile-time warnings for consumers building on modern frameworks.<\/p>\n<p><strong>Migration guidance<\/strong><\/p>\n<ul>\n<li>No action required for most consumers.<\/li>\n<li>If you previously relied on these constructors or methods for custom serialization, review your code and adopt recommended .NET patterns for serialization.<\/li>\n<\/ul>\n<h3><code>IEdmOperation<\/code> now uses <code>IEdmOperationReturn<\/code><\/h3>\n<p><strong>Summary<\/strong><\/p>\n<p>We\u2019ve modernized the EDM API by replacing the <code>ReturnType<\/code> property on <code>IEdmOperation<\/code> with an <code>IEdmOperationReturn<\/code> instance. This change aligns the interface with the design introduced in OData .NET 7.x and completes the transition planned for 9.x.<\/p>\n<p><strong>Details<\/strong><\/p>\n<ul>\n<li><strong>Before (7.x):<\/strong> Introduced <code>IEdmOperationReturn<\/code> but kept <code>IEdmOperation.ReturnType<\/code> for compatibility.<\/li>\n<li><strong>8.x:<\/strong> Marked <code>ReturnType<\/code> as <code>[Obsolete]<\/code> and recommended using <code>GetReturn()<\/code> extension method.<\/li>\n<li><strong>Now (9.x):<\/strong> Removed <code>ReturnType<\/code> and <code>GetReturn()<\/code>. <code>IEdmOperation<\/code> directly exposes <code>IEdmOperationReturn<\/code> via its <code>Return<\/code> property.<\/li>\n<\/ul>\n<p>The <code>IEdmOperationReturn<\/code> interface provides:<\/p>\n<pre class=\"prettyprint language-cs language-csharp\"><code class=\"language-cs language-csharp\">public interface IEdmOperationReturn : IEdmElement, IEdmVocabularyAnnotatable\r\n{\r\n    IEdmTypeReference Type { get; }\r\n    IEdmOperation DeclaringOperation { get; }\r\n}<\/code><\/pre>\n<p><strong>Migration guidance<\/strong><\/p>\n<ul>\n<li>Replace any usage of <code>ReturnType<\/code> with <code>Return.Type<\/code>.<\/li>\n<li>Remove calls to <code>GetReturn()<\/code>; use the new <code>Return<\/code> property instead.<\/li>\n<li>If you previously annotated return types via <code>GetReturn()<\/code>, apply annotations directly on <code>IEdmOperationReturn<\/code>.<\/li>\n<\/ul>\n<p><strong>Example<\/strong><\/p>\n<pre class=\"prettyprint language-cs language-csharp\"><code class=\"language-cs language-csharp\">\/\/ Previous:\r\nvar returnType = operation.ReturnType;\r\nvar returnInfo = operation.GetReturn();\r\n\r\n\/\/ New:\r\nvar returnType = operation.Return.Type;<\/code><\/pre>\n<h3>Support for Key Aliases in Entity Types<\/h3>\n<p><strong>Summary<\/strong><\/p>\n<p>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.<\/p>\n<p><strong>Details<\/strong><\/p>\n<p>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 &#8211; Key) requires that when a key property is a path (e.g., a property of a complex type), the key <strong>must<\/strong> specify an alias. This alias must be:<\/p>\n<ul>\n<li>A simple identifier.<\/li>\n<li>Unique within the declaring entity type and its base types.<\/li>\n<li>Not defined for primitive properties directly on the entity type.<\/li>\n<\/ul>\n<p><strong>Example: Key alias in CSDL XML<\/strong><\/p>\n<pre class=\"prettyprint language-xml\"><code class=\"language-xml\">&lt;EntityType Name=\"Category\"&gt;\r\n  &lt;Key&gt;\r\n    &lt;PropertyRef Name=\"Info\/ID\" Alias=\"EntityInfoID\" \/&gt;\r\n  &lt;\/Key&gt;\r\n  &lt;Property Name=\"Info\" Type=\"Sales.EntityInfo\" Nullable=\"false\" \/&gt;\r\n  &lt;Property Name=\"Name\" Type=\"Edm.String\" \/&gt;\r\n&lt;\/EntityType&gt;\r\n&lt;ComplexType Name=\"EntityInfo\"&gt;\r\n  &lt;Property Name=\"ID\" Type=\"Edm.Int32\" Nullable=\"false\" \/&gt;\r\n  &lt;Property Name=\"Created\" Type=\"Edm.DateTimeOffset\" \/&gt;\r\n&lt;\/ComplexType&gt;<\/code><\/pre>\n<p><strong>Why this matters<\/strong><\/p>\n<p>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.<\/p>\n<p><strong>Migration guidance<\/strong><\/p>\n<ul>\n<li>No breaking changes for existing models using simple keys.<\/li>\n<li>For models with complex keys, you can now define aliases in CSDL and consume them via ODL.<\/li>\n<li>Ensure aliases are unique and follow spec rules.<\/li>\n<\/ul>\n<h3>Removed <code>CsdlTarget<\/code> Support<\/h3>\n<p><strong>Summary<\/strong><\/p>\n<p>We have removed support for <code>CsdlTarget<\/code> 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.<\/p>\n<p><strong>Details<\/strong><\/p>\n<p>In earlier versions, ODL supported both OData CSDL and EF6 CSDL (used in EDMX files). <code>CsdlTarget<\/code> 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 <code>CsdlTarget<\/code> would introduce inconsistency and unnecessary complexity. In summary:<\/p>\n<ul>\n<li>EF CSDL generation is no longer supported.<\/li>\n<li>ODL focuses exclusively on OData CSDL for .NET Core scenarios.<\/li>\n<\/ul>\n<p><strong>Migration guidance<\/strong><\/p>\n<ul>\n<li>Remove any references to <code>CsdlTarget<\/code> in your code.<\/li>\n<li>If you previously generated EF CSDL, note that EF Core does not support this format. Use EF Core\u2019s built\u2011in model APIs instead.<\/li>\n<\/ul>\n<h3><code>IEdmModel.Find*<\/code> Overloads for <code>ReadOnlySpan&lt;char&gt;<\/code> \u2014 faster, allocation\u2011free lookups<\/h3>\n<p><strong>Summary<\/strong><\/p>\n<p>We added <code>ReadOnlySpan&lt;char&gt;<\/code> overloads to common <code>IEdmModel<\/code> lookup methods (e.g., <code>FindEntitySet<\/code>, <code>FindDeclaredType<\/code>, <code>FindDeclaredOperations<\/code>). 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.<\/p>\n<p><strong>Details<\/strong><\/p>\n<p>Historically, model lookups accepted only <code>string<\/code> parameters, so callers had to split a path like <code>\"Sales\/Orders\/ByDate\"<\/code> into multiple substrings to look up individual segments. By introducing span\u2011based overloads \u2014 e.g., <code>IEdmModel.FindEntitySet(ReadOnlySpan&lt;char&gt; entitySetName)<\/code> \u2014 callers can slice and probe the original buffer directly (no intermediate <code>string<\/code> allocations). This aligns with prior internal proposals to adopt <code>ReadOnlySpan&lt;char&gt;<\/code> across ODL parsing code paths to avoid <code>new string(...)<\/code> churn.<\/p>\n<p><strong>Example<\/strong><\/p>\n<pre class=\"prettyprint language-cs language-csharp\"><code class=\"language-cs language-csharp\">\/\/ Before: substring allocation\r\nvar entitySetName = path.Substring(start, length);\r\nvar entitySet = model.FindDeclaredEntitySet(entitySetName);\r\n\r\n\/\/ Now: allocation-free slice\r\nvar entitySetName = segmentText.AsSpan(start, length);\r\nvar entitySet = model.FindDeclaredEntitySet(entitySetName);<\/code><\/pre>\n<p>This pattern generalizes to <code>FindDeclaredType<\/code>, <code>FindDeclaredOperations<\/code>, and related utilities. See existing string\u2011only methods in the public API surface for context. https:\/\/learn.microsoft.com\/dotnet\/api\/microsoft.odata.edm.iedmmodel?view=odata-edm-7.0<\/p>\n<p><strong>Why this matters<\/strong><\/p>\n<p><code>ReadOnlySpan&lt;char&gt;<\/code> provides a stack\u2011allocated 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.<\/p>\n<p><strong>Migration guidance<\/strong><\/p>\n<ul>\n<li><strong>No breaking changes<\/strong>: Existing <code>string<\/code> overloads remain.<\/li>\n<li><strong>Optional optimization<\/strong>: Where you already compute indices into larger buffers (e.g., OData path segments), switch to <code>AsSpan(start, length)<\/code> and call the new overloads to reduce allocations.<\/li>\n<li><strong>Framework note<\/strong>: Span APIs are first\u2011class 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.<\/li>\n<\/ul>\n<h3>Untyped Numeric Deserialization<\/h3>\n<p><strong>Summary<\/strong><\/p>\n<p>We improved deserialization for untyped values so that common primitive numerics (e.g., <code>int<\/code>, <code>long<\/code>, <code>double<\/code>, <code>float<\/code>) and collections are handled correctly. A compatibility flag \u2014 <strong><code>ReadUntypedNumericAsDecimal<\/code><\/strong> \u2014 lets you choose between preserving legacy behaviour (numbers as <code>decimal<\/code>, e.g., for precision) or inferring more specific numeric CLR types.<\/p>\n<p><strong>Details<\/strong><\/p>\n<p>Historically, ODL\u2019s untyped value handling only recognized <code>bool<\/code> and <code>string<\/code>; all other numerics defaulted to <code>decimal<\/code>. That led to surprising results such as <code>42<\/code> materializing as <code>System.Decimal<\/code> rather than <code>System.Int32<\/code> for open\/dynamic properties. The new implementation expands support to infer numeric types when appropriate and adds a feature flag to control the behavior.<\/p>\n<ul>\n<li><strong>Default (type inference)<\/strong>: <code>ReadUntypedNumericAsDecimal = false<\/code>Untyped numerics are inferred and converted to specific CLR types (<code>int<\/code>, <code>long<\/code>, <code>double<\/code>, <code>float<\/code>) based on the runtime value, aligning deserialization with caller expectations for well\u2011known numeric values.<\/li>\n<li><strong>Alternative (precision\u2011preserving)<\/strong>: <code>ReadUntypedNumericAsDecimal = true<\/code>Untyped numerics are preserved as <code>decimal<\/code> to avoid precision loss, keeping wire compatibility with existing services that depend on decimal materialization.<\/li>\n<\/ul>\n<p><strong>Example<\/strong><\/p>\n<pre class=\"prettyprint language-cs language-csharp\"><code class=\"language-cs language-csharp\">var messageReaderSettings = new ODataMessageReaderSettings\r\n{\r\n    \/\/ Toggle compatibility flag to preserve untyped numerics as decimal\r\n    LibraryCompatibility = ODataLibraryCompatibility.ReadUntypedNumericAsDecimal\r\n};<\/code><\/pre>\n<p><strong>Note<\/strong>: In ambiguous cases without type information (no declared property and no <code>@odata.type<\/code>), the OData JSON spec describes heuristics for interpreting values. Historical behavior in ODL favored <code>decimal<\/code>; adopting <code>double<\/code> would be more spec\u2011aligned in some scenarios, but could be a silent breaking change. The new flag provides a controlled opt\u2011in path.<\/p>\n<p><strong>Why this matters<\/strong><\/p>\n<ul>\n<li>Fewer surprises for dynamic\/open types \u2013 well\u2011known numerics deserialize to expected CLR types.<\/li>\n<li>Precision\u2011sensitive workflows can toggle legacy decimal behavior, minimizing compatibility risk.<\/li>\n<li>The feature\u2011flag approach avoids wire semantics regressions while enabling forward\u2011compatible adoption.<\/li>\n<\/ul>\n<p><strong>Migration guidance<\/strong><\/p>\n<ul>\n<li><strong>Evaluate your dynamic\/untyped usage<\/strong>: For scenarios expecting <code>int<\/code>\/<code>long<\/code>\/<code>double<\/code>, this will be the default behaviour. Verify downstream type checks and JSON serialization remain consistent.<\/li>\n<li><strong>Opt\u2011in to legacy decimal behaviour if necessary<\/strong>: If you&#8217;d like your code to assume <code>decimal<\/code> for untyped numerics (e.g., financial calculations), opt\u2011in to the behaviour by use of <code>ReadUntypedNumericAsDecimal<\/code> compatibility flag.<\/li>\n<\/ul>\n<h3>.NET 10 Compatibility: Avoid <code>System.MemoryExtensions.Contains<\/code> in LINQ Expression Trees<\/h3>\n<p><strong>Summary<\/strong><\/p>\n<p>We addressed a breaking change introduced in .NET 10 where LINQ expressions involving <code>Contains<\/code> on arrays now compile to use <code>System.MemoryExtensions.Contains<\/code> with <code>ReadOnlySpan&lt;T&gt;<\/code>. This method cannot be translated into OData query expressions because <code>ReadOnlySpan&lt;T&gt;<\/code> is a <code>ref struct<\/code> and its implicit conversion introduces stack pointers, causing <code>NotSupportedException<\/code> during expression tree processing.<\/p>\n<p><strong>Details<\/strong><\/p>\n<ul>\n<li><strong>Behavior in .NET 8\/9<\/strong>LINQ <code>Contains<\/code> calls compiled to <code>System.Linq.Enumerable.Contains<\/code> on an array:\n<pre class=\"prettyprint language-cs language-csharp\"><code class=\"language-cs language-csharp\">.Call System.Linq.Enumerable.Contains(\r\n    .NewArray System.String[] { \"Milk\" },\r\n    $p.Name)<\/code><\/pre>\n<\/li>\n<li><strong>Behavior in .NET 10<\/strong>The same query now compiles to:\n<pre class=\"prettyprint language-cs language-csharp\"><code class=\"language-cs language-csharp\">.Call System.MemoryExtensions.Contains(\r\n    .Call System.ReadOnlySpan`1[System.String].op_Implicit(.NewArray System.String[] { \"Milk\" }),\r\n    $p.Name)<\/code><\/pre>\n<p>This introduces a <code>ReadOnlySpan&lt;T&gt;<\/code> conversion, which is unsupported in reflection\u2011based invocation because the declaring type contains stack pointers. The runtime throws:<\/p>\n<blockquote><p>System.NotSupportedException: Specified method is not supported.<\/p><\/blockquote>\n<\/li>\n<\/ul>\n<p><strong>Fix<\/strong><\/p>\n<p>ODL now detects and avoids translating <code>MemoryExtensions.Contains<\/code> in expression trees. Instead, it falls back to the safe pattern using <code>Enumerable.Contains<\/code> for query translation, ensuring compatibility across .NET versions.<\/p>\n<p><strong>Why this matters<\/strong><\/p>\n<p>Without this fix, OData LINQ queries using <code>Contains<\/code> would fail under .NET 10 with <code>NotSupportedException<\/code>. This change restores predictable behavior and prevents runtime crashes when upgrading to .NET 10.<\/p>\n<p><strong>Migration guidance<\/strong><\/p>\n<ul>\n<li>No code changes required for most consumers.<\/li>\n<li>If you implement custom expression visitors or rely on <code>MemoryExtensions.Contains<\/code> in query translation, ensure you handle or avoid <code>ReadOnlySpan&lt;T&gt;<\/code> conversions.<\/li>\n<li>For performance\u2011critical in\u2011memory filtering, you can still use <code>MemoryExtensions.Contains<\/code> outside of expression trees.<\/li>\n<\/ul>\n<h3>Removal of <code>ReadUntypedAsString<\/code> and Alignment with Standard Untyped Handling<\/h3>\n<p><strong>Summary<\/strong><\/p>\n<p>ODL 9 removes the <code>ReadUntypedAsString<\/code> setting from <code>ODataMessageReaderSettings<\/code>. This setting allowed untyped values to be read as raw JSON strings (<code>ODataUntypedValue<\/code>), enabling payload patterns that deviate from the OData specification. Starting with ODL 9, untyped values are always materialized as structured types (<code>ODataResource<\/code>, <code>ODataCollectionValue<\/code>, <code>ODataPrimitiveValue<\/code>) based on their actual content. This change enforces spec\u2011compliant payload structures and eliminates ambiguity in annotation placement.<\/p>\n<p><strong>Why this matters<\/strong><\/p>\n<p>This is <strong>not just an API\u2011breaking change<\/strong> \u2013 it can also break payloads that relied on the old behavior. Specifically:<\/p>\n<ul>\n<li>Previously, annotations for untyped structured properties could appear as property\u2011level annotations (e.g., <code>undeclaredComplex1@my.Annotation1<\/code>).<\/li>\n<li>Under the new logic, annotations for structured values must appear inside the structured object (e.g., <code>\"undeclaredComplex1\": { \"@my.Annotation1\": \"...\" }<\/code>).<\/li>\n<li>Old payloads using property\u2011level annotations for structured values will fail to deserialize under ODL 9.<\/li>\n<\/ul>\n<p><strong>Implications<\/strong><\/p>\n<ul>\n<li><strong>Roundtrip changes<\/strong>: Reading and writing untyped structured properties now produces payloads with annotations embedded inside the object, not alongside the property name.<\/li>\n<li><strong>Validation tightening<\/strong>: Invalid patterns (e.g., <code>@odata.type<\/code> as a property annotation) will throw exceptions unless corrected.<\/li>\n<li><strong>Removed configuration<\/strong>: <code>ReadUntypedAsString<\/code> is gone; untyped collections and complex values are always parsed into structured representations.<\/li>\n<\/ul>\n<p><strong>Example<\/strong><\/p>\n<p><em>Previous (ODL \u2264 8.x, <code>ReadUntypedAsString=true<\/code>):<\/em><\/p>\n<pre class=\"prettyprint language-json\"><code class=\"language-json\">{\r\n  \"undeclaredComplex1@my.Annotation1\": \"custom annotation value\",\r\n  \"undeclaredComplex1\": {\r\n    \"MyProp1\": \"aaaaaaaaa\"\r\n  }\r\n}<\/code><\/pre>\n<p><em>New (ODL 9):<\/em><\/p>\n<pre class=\"prettyprint language-json\"><code class=\"language-json\">{\r\n  \"undeclaredComplex1\": {\r\n    \"@my.Annotation1\": \"custom annotation value\",\r\n    \"MyProp1\": \"aaaaaaaaa\"\r\n  }\r\n}<\/code><\/pre>\n<p><strong>Migration guidance<\/strong><\/p>\n<ol>\n<li><strong>Audit payloads<\/strong>: If your clients or services emit annotations for structured properties as property\u2011level annotations, update them to embed annotations inside the structured object.<\/li>\n<li><strong>Plan for coordination<\/strong>: This change affects wire format. Services upgrading to ODL 9 must coordinate with clients to ensure payloads conform to the new structure.<\/li>\n<li><strong>Versioning strategy<\/strong>: Consider endpoint versioning or phased rollout if you cannot synchronize all clients immediately.<\/li>\n<\/ol>\n<p><strong>Why we made this change<\/strong><\/p>\n<ul>\n<li>The old behavior was non\u2011compliant with the OData spec and introduced inconsistencies.<\/li>\n<li><code>ReadUntypedAsString<\/code> was rarely used and primarily supported ill\u2011formed payloads.<\/li>\n<li>Removing this feature simplifies the reader\/writer pipeline and ensures predictable, spec\u2011aligned behavior.<\/li>\n<\/ul>\n<h3>Refactored Transcoding Logic \u2014 Use <code>Encoding.CreateTranscodingStream<\/code><\/h3>\n<p><strong>Summary<\/strong><\/p>\n<p>ODL 9 replaces the custom <code>TranscodingWriteStream<\/code> implementation with the built\u2011in <code>Encoding.CreateTranscodingStream<\/code> API introduced in .NET 8. This change simplifies the codebase, reduces maintenance overhead, and leverages the optimized framework implementation for encoding conversion.<\/p>\n<p><strong>Details<\/strong><\/p>\n<ul>\n<li><strong>Previous behavior<\/strong>: ODL shipped its own <code>TranscodingWriteStream<\/code> to handle scenarios where payloads needed to be written in a different encoding than the underlying stream.<\/li>\n<li><strong>New behavior<\/strong>: The library now uses:\n<pre class=\"prettyprint language-cs language-csharp\"><code class=\"language-cs language-csharp\">var transcodingStream = Encoding.CreateTranscodingStream(outputStream, sourceEncoding, targetEncoding);<\/code><\/pre>\n<p>This API is part of the .NET standard library and provides a robust, well\u2011tested solution for transcoding streams.<\/li>\n<\/ul>\n<p><strong>Why this matters<\/strong><\/p>\n<ul>\n<li>Removes custom code that duplicated framework functionality.<\/li>\n<li>Improves performance and reliability by using the optimized .NET implementation.<\/li>\n<li>Reduces complexity for future maintenance.<\/li>\n<\/ul>\n<p><strong>Migration guidance<\/strong><\/p>\n<ul>\n<li>No breaking changes for consumers \u2014 behavior remains consistent.<\/li>\n<\/ul>\n<h3>Modernized Date and Time Handling \u2014 Use <code>System.DateOnly<\/code> and <code>System.TimeOnly<\/code><\/h3>\n<p><strong>Summary<\/strong><\/p>\n<p>ODL 9 replaces legacy EDM primitives <code>Microsoft.OData.Edm.Date<\/code> and <code>Microsoft.OData.Edm.TimeOfDay<\/code> with .NET\u2019s built\u2011in <code>System.DateOnly<\/code> and <code>System.TimeOnly<\/code>. This aligns OData .NET with modern .NET standards and improves consistency with framework APIs.<\/p>\n<p><strong>Details<\/strong><\/p>\n<ul>\n<li><strong>Internal refactor<\/strong>: All usages of <code>Edm.Date<\/code> and <code>Edm.TimeOfDay<\/code> have been replaced with <code>DateOnly<\/code> and <code>TimeOnly<\/code>.<\/li>\n<li><strong>Function and property names<\/strong>: Updated to match .NET naming conventions:\n<ul>\n<li><code>hours<\/code> \u2192 <code>hour<\/code><\/li>\n<li><code>minutes<\/code> \u2192 <code>minute<\/code><\/li>\n<li><code>seconds<\/code> \u2192 <code>second<\/code><\/li>\n<li><code>milliseconds<\/code> \u2192 <code>millisecond<\/code><\/li>\n<\/ul>\n<\/li>\n<li><strong>Error handling<\/strong>: Invalid time values now throw <code>ArgumentOutOfRangeException<\/code> (from <code>TimeOnly<\/code>) instead of <code>FormatException<\/code>. Error messages originate from .NET APIs for consistency.<\/li>\n<li><strong>String formatting<\/strong>Previous: <code>TimeSpan.ToString(@\\\"hh\\\\:mm\\\\:ss\\\\.fffffff\\\", CultureInfo.InvariantCulture)<\/code>\n<p>Now: <code>TimeOnly.ToString(@\\\"HH\\\\:mm\\\\:ss\\\\.fffffff\\\", CultureInfo.InvariantCulture)<\/code><\/li>\n<\/ul>\n<p><strong>Example<\/strong><\/p>\n<pre class=\"prettyprint language-cs language-csharp\"><code class=\"language-cs language-csharp\">int hour = 20, minute = 30, second = 59, millisecond = 1;\r\nvar timeOnly = new TimeOnly(hour, minute, second, millisecond);\r\nConsole.WriteLine(timeOnly.ToString(@\"HH:mm:ss.fffffff\", CultureInfo.InvariantCulture));\r\n\/\/ Output: 20:30:59.0010000<\/code><\/pre>\n<p><strong>Spec alignment<\/strong><\/p>\n<p>The OData spec requires <code>Date<\/code> and <code>TimeOfDay<\/code> values to conform to <code>dateValue<\/code> and <code>timeOfDayValue<\/code> rules. <code>DateOnly<\/code> and <code>TimeOnly<\/code> follow these rules, ensuring protocol compliance:<\/p>\n<ul>\n<li>https:\/\/docs.oasis-open.org\/odata\/odata-csdl-json\/v4.01\/odata-csdl-json-v4.01.html#sec_DateOData Spec &#8211; Date<\/li>\n<li>https:\/\/docs.oasis-open.org\/odata\/odata-csdl-json\/v4.01\/odata-csdl-json-v4.01.html#sec_TimeofDayOData Spec &#8211; TimeOfDay<\/li>\n<\/ul>\n<p><strong>Migration guidance<\/strong><\/p>\n<ul>\n<li>If you reference <code>Edm.Date<\/code> or <code>Edm.TimeOfDay<\/code> directly, update your code to use <code>DateOnly<\/code> and <code>TimeOnly<\/code>.<\/li>\n<li>Validate any custom serialization logic to ensure it uses the correct format strings.<\/li>\n<li>Exception handling may need updates if you previously caught <code>FormatException<\/code>.<\/li>\n<\/ul>\n<h2>Summary<\/h2>\n<p>ODL 9 Preview 3 focuses on safety, compatibility, and modern .NET alignment with a small set of high\u2011impact changes:<\/p>\n<ul>\n<li><strong>Safer defaults:<\/strong> Action queries now return <code>null<\/code> for nullable\/reference types instead of throwing, while preserving fail\u2011fast behavior for non\u2011nullable value types.<\/li>\n<li><strong>Spec\u2011aligned untyped handling:<\/strong> Removed <code>ReadUntypedAsString<\/code>; untyped values are always materialized as structured types, and annotations for structured values are embedded inside the object.<\/li>\n<li><strong>Performance &amp; ergonomics:<\/strong> Added <code>ReadOnlySpan&lt;char&gt;<\/code> overloads for model lookups and replaced custom transcoding with <code>Encoding.CreateTranscodingStream<\/code>.<\/li>\n<li><strong>Modern primitives:<\/strong> Switched to <code>System.DateOnly<\/code>\/<code>System.TimeOnly<\/code> for date\/time, and fixed .NET 10 LINQ expression translation to avoid span\u2011related runtime issues.<\/li>\n<li><strong>Compatibility cleanup:<\/strong> Addressed .NET 8 obsoletion warnings and removed legacy EF CSDL target support; the library now targets modern .NET runtimes.<\/li>\n<\/ul>\n<p>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.<\/p>\n<\/article>\n","protected":false},"excerpt":{"rendered":"<p>We\u2019re happy to announce that OData .NET (ODL) 9 Preview 3 has been officially released and is available on NuGet: 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\u2019s New in OData .NET (ODL) 9 Preview 3 The OData .NET (ODL) 9.0.0 Preview 3 introduces a set [&hellip;]<\/p>\n","protected":false},"author":25333,"featured_media":3253,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-5963","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-odata"],"acf":[],"blog_post_summary":"<p>We\u2019re happy to announce that OData .NET (ODL) 9 Preview 3 has been officially released and is available on NuGet: 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\u2019s New in OData .NET (ODL) 9 Preview 3 The OData .NET (ODL) 9.0.0 Preview 3 introduces a set [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/5963","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/users\/25333"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/comments?post=5963"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/5963\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/media\/3253"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/media?parent=5963"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/categories?post=5963"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/tags?post=5963"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}