- Microsoft.OData.Core 9.0.0 Preview 2
- Microsoft.OData.Edm 9.0.0 Preview 2
- Microsoft.Spatial 9.0.0 Preview 2
- Microsoft.OData.Client 9.0.0 Preview 2
What’s New in OData .NET (ODL) 9 Preview 1 & 2
The OData .NET (ODL) 9.0 previews introduce 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 the first two preview releases.
Targeting .NET 10 Preview
ODL now builds against .NET 10 preview, ensuring compatibility with the upcoming runtime. This helps early adopters validate OData workloads on the latest platform.
Removed JSONP Support
JSONP (JSON with Padding) was an early workaround for browsers’ same-origin policy before CORS became widely adopted. It allowed a client to request JSON data from another domain by wrapping the payload inside a JavaScript function call.
For example, consider a normal OData JSON response:
{
"@odata.context": "http://localhost/$metadata#Orders",
"value": [
{ "Id": 1, "Status": "Pending" },
{ "Id": 2, "Status": "Shipped" }
]
}
With JSONP enabled and a callback function defined, the same response could be returned as:
processOrders({
"@odata.context": "http://localhost/$metadata#Orders",
"value": [
{ "Id": 1, "Status": "Pending" },
{ "Id": 2, "Status": "Shipped" }
]
});
This allowed the client’s browser to execute the function processOrders
with the JSON payload as its
argument, bypassing cross-domain restrictions.
Why It Was Removed
- Security concerns: JSONP allows arbitrary JavaScript execution, which can be exploited in XSS-style attacks.
- Superseded by CORS: Modern browsers and frameworks natively support Cross-Origin Resource Sharing (CORS), which provides a secure and standards-based way to enable cross-domain requests.
- Low usage: Very few ODL consumers relied on JSONP in recent years.
By removing JSONP support, ODL 9 reduces attack surface and simplifies the codebase, without impacting the majority of modern OData clients. Developers needing cross-domain access should instead configure CORS, which is the modern, secure alternative.
Case-Insensitive URI Resolver by Default
Previously, consumers had to explicitly enable case-insensitive handling for query options like $filter
,
$select
, etc.
According to the
OData V4.01 specification,
services MUST support case-insensitive system query option names, whether or not they are prefixed with
$
.
ASP.NET Core OData already complies with this requirement by injecting a case-insensitive
UnqualifiedODataUriResolver
by default. To ensure consistent behavior across the OData ecosystem, ODL now defaults
EnableCaseInsensitive = true
when creating a new instance of ODataUriResolver
:
ODataUriResolver.cs
private static readonly ODataUriResolver Default = new ODataUriResolver(); // EnableCaseInsensitive is now true by default
Example
With this change, query option names are parsed equivalently regardless of casing. For instance, both of the following requests now succeed and are interpreted the same way:
GET https://localhost:54001/odata/Books?$expand=Authors
GET https://localhost:54001/odata/Books?$expand=authors
Benefits
- Spec compliance: Matches the OData V4.01 requirement.
- Consistency: Aligns ODL behavior with ASP.NET Core OData defaults.
- Developer experience: Eliminates common case-sensitivity pitfalls in query construction.
Improved CSDL Type Name Validation
ODL now performs stricter validation of type names when parsing CSDL documents. This ensures that only valid type names are accepted and that clear, consistent error messages are produced when invalid cases are encountered.
The updated logic rejects malformed or unsupported type names, closing gaps where previously invalid inputs could slip through silently.
What Changed
Validation checks have been enhanced to catch issues such as:
- Unmatched or misplaced parentheses
ns.myType)
Collection(ns.myType
(ns.myType
- Unsupported type prefixes
Enumerable(ns.myType)
- Invalid suffixes or extra characters
Collection(ns.myType)s
Collection)ns.myType()
- Empty or whitespace-only type names
- Invalid use of
Collection
orRef
- Collection()
- Ref
- Ref()
Why It Matters
- Correctness: Prevents invalid EDM models from being accepted, which could otherwise cause subtle runtime issues.
- Clarity: Developers get clear, actionable error messages instead of confusing parsing behavior.
- Interoperability: Ensures models parsed by ODL conform more strictly to OData CSDL standards, improving tooling and ecosystem alignment.
Removed Deprecated Flags and Properties
KeyComparisonGeneratesFilterQuery
In ODL 9 we removed the DataServiceContext.KeyComparisonGeneratesFilterQuery
flag entirely. The OData .NET
Client now always translates a LINQ Where
that compares key properties into a $filter
query,
and it reserves the key segment form for explicit ByKey(...)
calls.
What Changed
Historically, if you had the expression:
// Single-key entity
DataServiceContextInstance.Customers.Where(c => c.Id == 13)
where Id
is the key property, the client would emit a key segment URL:
/Customers(13)
instead of the natural filter form:
/Customers?$filter=Id eq 13
This would happen so long as the properties referenced in the Where
expression predicate comprised of only key properties.
Why This Matters
The inconsistent behavior meant that a simple Where
could silently depend on a “get-by-key” executable
endpoint on the server (e.g., Get(int key)
), while in other cases it wouldn’t — an inconsistency that
surprised developers and produced hard-to-diagnose bugs.
This overlap created confusion because Where
is naturally a filtering operation and should translate to $filter
.
The canonical way to express “get-by-key” semantics has always been through the dedicated ByKey
API.
// ByKey semantics require a dedicated get-by-key endpoint
[EnableQuery]
public ActionResult<Customer> Get(int key) { ... }
// Whereas $filter works fine with a queryable Get endpoint
[EnableQuery]
public ActionResult<IQueryable<Customer>> Get() { ... }
By enforcing the distinction, ODL avoids subtle bugs and makes it explicit when a key-based endpoint is required.
Developers upgrading from 8.x should review their LINQ queries to ensure that any intended key lookups use ByKey
explicitly.
Deprecation Path
- ODL 7.x: Introduced the
KeyComparisonGeneratesFilterQuery
flag (default:false
) to avoid an immediate breaking change. - ODL 8.x: Changed the default to
true
, soWhere
expressions would generate$filter
unless explicitly toggled back. - ODL 9.x: Removed the flag entirely. Developers must now use
ByKey
for key lookups, andWhere
will consistently generate$filter
.
Benefits
- Clarity: Developers know exactly when they’re generating
ByKey
versus$filter
queries. - Consistency: Eliminates situations where the same LINQ query might require different service endpoints.
- Correctness: Prevents hidden coupling between query translation and service controller design.
Timeout Property
The obsolete DataServiceContext.Timeout
property has been fully removed. As recommended since 8.x, configure
timeouts on your HttpClient
via IHttpClientFactory
and related handlers, rather than on the
OData client surface. This keeps HTTP concerns in the HTTP layer and aligns with modern .NET guidance and our 8.x
deprecation notes
Reduced Redundant Payload Annotations
When generating payloads, the OData client would serialize @odata.type
annotations even for
declared properties and non-derived instances. While technically valid, these annotations inflated payload
sizes and added noise without providing additional value.
Example
Consider an Order entity with declared properties like Status
, Tags
, and ShippingAddress
. Previously, serializing such an entity in OData client produced redundant annotations:
{
"@odata.type": "#Ns.Models.Order",
"Id": 1,
"Status@odata.type": "#Ns.Models.OrderStatus",
"Status": "Pending",
"Tags@odata.type": "#Collection(String)",
"Tags": ["Urgent"],
"ShippingAddress": {
"@odata.type": "#Ex340.Models.Address",
"Street": "123 Main St"
}
}
Here, the annotations are unnecessary because:
Status
is a declared enum property.Tags
is a declared collection of strings.ShippingAddress
is a declared complex type.
What Changed
With ODL 9, these redundant @odata.type
annotations are omitted unless:
- The instance is of a derived type, or
- The property is dynamic (not declared in metadata).
The same payload is now serialized much more concisely:
{
"Id": 1,
"Status": "Pending",
"Tags": ["Urgent"],
"ShippingAddress": {
"Street": "123 Main St"
}
}
Benefits
- Smaller payloads: Reduced bandwidth usage and faster responses.
- Cleaner JSON: Easier to read, debug, and consume.
- Correctness preserved: Annotations still appear when needed for derived or dynamic values.
Removed Support for Nullable Generic Key Types
In earlier versions of the OData client, there was a bug that allowed nullable properties to be designated as key properties in an entity type definition. For example:
public class Person
{
public int? PersonId { get; set; } // Nullable key (incorrectly allowed)
public string Name { get; set; }
}
Why This Was a Problem
- Violation of the OData spec: Entity keys must always be non-nullable. A
null
key would make the identity of the entity undefined. - Potential runtime errors: Nullable keys could lead to invalid URIs or unpredictable server behavior.
- Silent inconsistency: Since OData client didn’t enforce correctness, developers could model data in ways that would later fail when interacting with an OData service.
Why the Fix Landed in ODL 9
Correcting this in a minor version would have been a breaking change, since previously accepted models would suddenly fail validation. To avoid disrupting existing consumers in patch or minor releases, the fix was deferred until ODL 9, where breaking changes are expected.
Benefits
- Correctness: Ensures entity keys comply with the OData specification.
- Predictability: Eliminates undefined edge cases caused by nullable keys.
- Clarity: Developers now get clear validation errors if they attempt to use a nullable key property.
Custom URI Extensions Registered per EDM Model
Another significant change in ODL 9 is how custom URI functions, literal prefixes, and literal parsers are registered. Previously, these were registered globally using static helpers:
CustomUriLiteralPrefixes.AddCustomLiteralPrefix("temp", EdmCoreModel.Instance.GetDouble(false));
CustomUriLiteralParsers.AddCustomUriLiteralParser(EdmCoreModel.Instance.GetDouble(false), new TemperatureUriLiteralParser());
CustomUriFunctions.AddCustomUriFunction(
"tofahrenheit",
new FunctionSignatureWithReturnType(
EdmCoreModel.Instance.GetDouble(false),
EdmCoreModel.Instance.GetDouble(false)));
This global registration caused problems for multi-model scenarios, where different EDM models might require different sets of extensions.
What Changed
In ODL 9, these are now registered per EDM model instead of globally. This improves isolation, avoids unintended conflicts, and gives fine-grained control.
For example:
var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Weather>("WeatherForecast");
var model = modelBuilder.GetEdmModel();
// Register extensions per model
model.AddCustomLiteralPrefix("temp", EdmCoreModel.Instance.GetDouble(false));
model.AddCustomUriLiteralParser(EdmCoreModel.Instance.GetDouble(false), new TemperatureUriLiteralParser());
model.AddCustomUriFunction(
"tofahrenheit",
new FunctionSignatureWithReturnType(
EdmCoreModel.Instance.GetDouble(false),
EdmCoreModel.Instance.GetDouble(false)));
- Custom URI Functions: Extend OData query semantics by adding custom functions usable in
$filter
,$orderby
, etc. (e.g., converting Celsius to Fahrenheit). - Custom URI Literal Prefixes: Allow introducing new literal formats (e.g.,
temp'79.7:Fahrenheit'
). - Custom URI Literal Parsers: Implement logic to parse and interpret these custom literals into CLR values.
Example in Action
Custom URI function usage:
GET http://localhost/WeatherForecast?$filter=tofahrenheit(Temperature) gt 70
Custom URI literal usage:
GET http://localhost/WeatherForecast?$filter=Temperature eq temp'79.7:Fahrenheit'
Benefits
- Isolation: Different models can have different custom functions and literals without clashing.
- Flexibility: Enables richer, domain-specific query expressions.
- Cleaner design: No more static/global state; behavior is tied to the model where it belongs.
Summary
The ODL 9 previews bring meaningful updates:
- Modern runtime support (targeting .NET 10).
- Removal of outdated features (JSONP, deprecated properties/flags).
- Usability improvements (case-insensitive resolver by default, reduced payload noise).
- More robust model-specific approach to custom URI functions, prefixes, and parsers.
These changes pave the way for a cleaner, more predictable OData experience moving forward. In future updates, we’ll provide deeper examples and migration guidance for developers upgrading to ODL 9.
Try the ODL 9 preview packages on NuGet and share feedback on GitHub repo.
0 comments
Be the first to start the discussion.