This documentation describes the latest OData library version 7.
For documentation of earlier versions of the OData Library, which are now in maintenance mode, please refer here.
1. CORE
-
1.1 Write OData payload
There are several kinds of OData payloads, including service document, model metadata, entity set, entity, entity reference(s), complex value(s), primitive value(s). OData Core library is designed to write and read all these payloads.
We’ll go through each kind of payload here. But first, let’s set up the necessary code that is common to all kinds of payloads.
Class
ODataMessageWriter
is the entrance class to write OData payloads.To construct an
ODataMessageWriter
instance, you’ll need to provide anIODataResponseMessage
, orIODataRequestMessage
, depending on if you are writing a response or a request.OData Core library provides no implementation of these two interfaces, because it is different in different scenarios.
In this tutorial, we’ll use the InMemoryMessage.cs.
We’ll use the model set up in the EDMLIB section.
Then set up the message to write the payload to.
Create the settings:
Now we are ready to create the
ODataMessageWriter
instance:After we have written the payload, we can inspect the memory stream wrapped in
InMemoryMessage
to check what has been written.Here is the complete program that uses
SampleModelBuilder
andInMemoryMessage
to write metadata payload:Now we’ll go through writing each kind of payload.
Write metadata document
Writing metadata is simple, just use
ODataMessageWriter.WriteMetadataDocument()
.Please notice that this API only works when:
- Writing a response message, i.e., when constructing
ODataMessageWriter
, you must supplyIODataResponseMessage
. - A model is supplied when constructing
ODataMessageWriter
.
So the following two examples won’t work.
Write service document
To write a service document, first create an
ODataServiceDocument
instance, which encapsulates all the necessary information in a service document, which includes entity sets, singletons, and function imports.In this example, we create a service document that contains two entity sets, one singleton, and one function import.
Then let’s call
WriteServiceDocument()
to write it.However, this would not work. An
ODataException
will be thrown saying that “The ServiceRoot property in ODataMessageWriterSettings.ODataUri must be set when writing a payload.” This is because a valid service document will contain a context URL referencing the metadata document URL which needs to be provided inODataMessageWriterSettings
.The service root information is provided in
ODataUri.ServiceRoot
:As you can see, you don’t need to provide a model to write a service document.
It takes efforts to instantiate a service document instance and set up the entity sets, singletons, and function imports. Actually, EdmLib provides a useful API which can generate an appropriately-filled service document instance from model. The API is
GenerateServiceDocument()
defined as an extension method onIEdmModel
:All the entity sets, singletons and function imports whose
IncludeInServiceDocument
attribute is set to true in the model will appear in the generated service document. And according to the spec, only those function imports without any parameters should set theirIncludeInServiceDocument
attribute to true.As with
WriteMetadataDocument()
,WriteServiceDocument()
works only when writing response messages.Besides
WriteServiceDocument()
, there is alsoWriteServiceDocumentAsync()
inODataMessageWriter
. It is the async version ofWriteServiceDocument()
, so you can call it in an async way:A lot of APIs in writer and reader provide async versions. They work as async counterparts to the APIs without the
Async
suffix.Write entity set
An entity set is a collection of entities. Unlike metadata or service document, you must create another writer from
ODataMessageWriter
to write an entity set. The library is designed to write entity set in a streaming/progressive way, which means the entities are written one by one.Entity set is represented by the
ODataResourceSet
class. To write an entity set, the following information needs to be provided:- The service root which is defined by
ODataUri
. - The model, as provided when constructing the
ODataMessageWriter
instance. - Entity set and entity type information.
Here is how to write an empty entity set using the old
WriteStart()/WriteEnd()
API (for the new writer API, see here).Line 4 gives the service root, line 6 gives the model, and line 10 gives the entity set and entity type information.
The output looks like
The output contains a context URL which is based on the service root provided in
ODataUri
and the entity set name. There is also a value which turns out to be an empty collection. It will hold the entities if there is any.There is another way to provide the entity set and entity type information, through
ODataResourceSerializationInfo
. This also eliminates the need to provide a model.When writing entity set, you can provide a next page used in server driven paging.
The output will then contain a next link before the value collection.
If you want the next link to appear after the value collection, you can set the next link after the
WriteStart()
call, but before theWriteEnd()
call.There is no additional requirement on next link, as long as it is a valid URL.
To write an entity in an entity set, create an
ODataResource
instance and callWriteStart()/WriteEnd()
on it in-between theWriteStart()/WriteEnd()
calls on the entity set.We’ll introduce more details on writing entities in the next section.
Write entity
Entities can be written in several places:
- As a top level entity.
- As an entity in an entity set.
- As an entity expanded within another entity.
To write a top level entity, use
ODataMessageWriter.CreateODataResourceWriter()
.We’ve already introduced in the previous section how to write entities in an entity set. Now we’ll look at how to write an entity expanded within another entity.
The output will contain an order entity nested within a customer entity.
- Writing a response message, i.e., when constructing
-
1.2 Fluent functional-style writer API
In the previous section, paired
WriteStart()
/WriteEnd()
calls have been made to write payloads. In this version, a new set of fluent functional-style API has been introduced as an improvement over the previous API which is rather primitive, requiring pairedWriteStart()
/WriteEnd()
calls.The new API replaces paired
WriteStart()
/WriteEnd()
calls with a singleWrite()
call.Write()
comes in two flavors. The first flavor takes a single argument which is the thing you want to write. For example,writer.Write(entry);
is equivalent toThe second flavor takes two arguments. The first argument is same as before. The second argument is an
Action
delegate which is to be invoked in-between writing the first argument. For instance,is equivalent to
In general, this new API should be preferred to the previous one.
-
1.3 Read OData payload
The reader API is similar to the writer API, and you can expect symmetry here.
First, we’ll set up the necessary code that is common to all kinds of payloads.
Class
ODataMessageReader
is the entrance class to read OData payloads.To construct an
ODataMessageReader
instance, you’ll need to provide anIODataResponseMessage
orIODataRequestMessage
, depending on if you are reading a response or a request.OData Core library provides no implementation of these two interfaces, because it is different in different scenarios.
In this tutorial, we’ll still use the InMemoryMessage.cs.
We’ll still use the model set up in the EDMLIB section.
Then set up the message to read the payload from.
Create the settings:
Now we are ready to create an
ODataMessageReader
instance:We’ll use the code in the previous section to write the payloads, and in this section use the reader to read them. After writing the payloads, we should set
MemoryStream.Position
to zero.Here is the complete program that uses
SampleModelBuilder
andInMemoryMessage
to write and then read a metadata document:Now we’ll go through each kind of payload.
Read metadata document
Reading metadata is simple, just use
ODataMessageReader.ReadMetadataDocument()
.Similar to writing a metadata document, this API only works when reading a response message, i.e., when constructing the
ODataMessageReader
, you must supplyIODataResponseMessage
.Read service document
Reading service document is through the
ODataMessageReader.ReadServiceDocument()
API.This works only when reading a response message.
There is another API
ODataMessageReader.ReadServiceDocumentAsync()
. It is the async version ofReadServiceDocument()
, and you can call it in an async way:Read entity set
To read an entity set, you must create another reader
ODataResourceSetReader
. The library is designed to read entity set in a streaming/progressive way, which means the entities are read one by one.Here is how to read an entity set.
Read entity
To read a top level entity, use
ODataMessageReader.CreateODataResourceReader()
. Except that, there is no difference compared to reading an entity set. -
1.4 Use ODataUriParser
This post is intended to guide you through the URI parser for OData V4, which is released with ODataLib V6.0 and later.
You may have already read the following posts about OData URI parser in ODataLib V5.x:
- Parsing $filter and $orderby using the ODataUriParser
- Parsing OData Paths, $select and $expand using the ODataUriParser
Parts of those articles, e.g., introductions to
ODataPath
andQueryNode
hierarchy, still apply to the V4 URI parser. In this post, we will deal with API changes and newly-added features.Overview
The main reference document for using URI parser is the URL Conventions specification. The
ODataUriParser
class is the main part of its implementation in ODataLib.The responsibility of
ODataUriParser
is two-fold:- Parse resource path
- Parse query options
We’ve also introduced the new
ODataQueryOptionParser
class in ODataLib V6.2+ to deal with the scenario where you do not have the full resource path and only want to parse the query options. TheODataQueryOptionParser
shares the same API signatures for parsing query options. You can find more information below.Using ODataUriParser
The use of
ODataUriParser
is easy and straightforward. As already mentioned, we do not support static methods now, so we will begin by creating anODataUriParser
instance.One
ODataUriParser
constructor is:Parameters:
model
is the data model the parser will refer to;serviceRoot
is the base URI for the service, which is constant for a particular service. Note thatserviceRoot
must be an absolute URI;uri
is the request URI to be parsed, including any query options. When it is an absolute URI, it must be based onserviceRoot
, or it can be a relative URI. In the following example, we use the model from OData V4 demo service, and create anODataUriParser
instance based on it.Parsing resource path
You can use the following API to parse resource path:
You don’t need to pass in resource path as a parameter to
ParsePath()
, because it has already been provided when constructing theODataUriParser
instance.ODataPath
holds an enumeration of path segments for the resource path. All path segments are represented by classes derived fromODataPathSegment
.In the example, the resource path in the request URI is
Products(1)
, so the resultingODataPath
will contain two segments: anEntitySetSegment
for the entity setProducts
, and aKeySegment
for the key with integer value1
.Parsing query options
ODataUriParser
supports parsing following query options:$select
,$expand
,$filter
,$orderby
,$search
,$top
,$skip
, and$count
.For the first five, the parsing result is represented by an instance of class
XXXClause
which presents the query option as an Abstract Syntax Tree (with semantic information bound). Note that$select
and$expand
query options are handled together by theSelectExpandClause
class. The latter three all have primitive type values, and the parsing results are represented by the corresponding nullable primitive types.For all query option parsing results, a null value indicates that the corresponding query option is not present in the request URI.
Here is an example for parsing the request URI with different kinds of query options (please notice that the value of
skip
would be null, since the skip query option is not present in the request URI):The data structures for
SelectExpandClause
,FilterClause
,OrdeyByClause
have already been presented in two previous articles mentioned in the beginning of this post. Here I’d like to talk about the newly-addedSearchClause
.SearchClause
contains a tree representation of the$search
query. The detailed rules of$search
query option can be found here. In general, the search query string can contain search terms combined with logic operators: AND, OR and NOT.All search terms are represented by
SearchTermNode
which is derived fromSingleValueNode
.SearchTermNode
has aText
property containing the original word or phrase.The
SearchClause.Expression
property holds the tree structure for$search
. If$search
contains a single word, theExpression
would be a singleSearchTermNode
. But when$search
contains a combination of various terms and logic operators,Expression
would also containBinaryOperatorNode
andUnaryOperatorNode
.For example, if the query option has the value
a AND b
, the result expression (syntax tree) would have the following structure:Using ODataQueryOptionParser
There may be cases where you already know the query context information, and does not have the full request URI. The
ODataUriParser
will not be available at this time, as it requires a full URI. The user would have to fake one.In ODataLib 6.2 we shipped a new URI parser that targets query options only. It requires the model and type information to be provided through its constructor, and then it could be used for query options parsing just as
ODataUriParser
.One of its constructors looks like this:
Parameters (here the target object refers to what resource path is addressed, see spec):
model
is the model the parser will refer to;targetEdmType
is the type of the target object, to which the query options apply;targetNavigationSource
is the entity set or singleton where the target comes from, and it is usually the navigation source of the target object;queryOptions
is a dictionary containing the key-value pairs for query options.Here is an example demonstrating its use. It is almost identical to that of
ODataUriParser
: -
1.5 Dependency Injection Support
From ODataLib v7.0, we introduced Dependency Injection (or “DI” in short) support to dramatically increase the extensibility of the library where users can plug in their custom implementations and policies in an elegant way. Introduction of DI can also simplify the API and implementation of ODataLib by eliminating redundant function parameters and class properties. Since ODataLib is a reusable library, we don’t take direct dependency on any existing DI framework. Instead we build and rely on an abstraction layer including several simple interfaces that decouples ODataLib from any concrete DI implementation. Users of ODataLib will be free to choose whatever DI framework they like to work with ODataLib.
Introduction to DI
For a complete understanding of the concept of DI and how it works in a typical ASP.NET Web application, please refer to the introduction from ASP.NET Core.
To make DI work properly with ODataLib, basically there are several things you have to do within your application:
- Implement your container builder based on your DI framework.
- Register the required services from both ODataLib and your application.
- Build and use the container (to retrieve the services) in ODataLib.
Implement Your Container Builder
Since ODataLib is based on .NET, we use the interface
IServiceProvider
from .NET Framework as the abstraction of “container”. The container itself is read-only (as you can see,IServiceProvider
only has aGetService
method) so we designed another interfaceIContainerBuilder
in ODataLib to build the container. Below is the source ofIContainerBuilder
:The first
AddService
method registers a service by its implementation type while the second one registers using a factory method. TheBuildContainer
method is used to build a container that implementsIServiceProvider
which contains all the services registered. The first parameter ofAddService
indicates the lifetime of the service you register. Below is the source ofServiceLifetime
. For the meaning of each member, please refer to the doc from ASP.NET Core.Once you have determined a specific DI framework to use in your application, you need implement a container builder from
IContainerBuilder
based on the DI framework you choose. In this tutorial, we will use the Microsoft DI Framework (the default DI implementation for ASP.NET Core) as an example. The implementation of the container builder should more or less look like below:Basically what the
TestContainerBuilder
does is delegating the service registrations to the innerServiceCollection
which comes from the Microsoft DI Framework. By the way, this is the exact implementation ofIContainerBuilder
we use in Web API OData v6.x :-)Of course, the APIs of
IContainerBuilder
are kind of “primitive” thus they are not so convenient when directly used to register services. That’s what we are going to address in the next section.Register the Required Services
Once you have your container builder, the next step is to register the required services into the container. We defined many extension methods in
ContainerBuilderExtensions
toIContainerBuilder
to ease the service registration. Below are the signatures of the extension methods and their corresponding examples.For the usage of the
AddService
overloads, please see the comments for examples. ForAddServicePrototype
, we currently only support the following service types:ODataMessageReaderSettings
,ODataMessageWriterSettings
andODataSimplifiedOptions
. This design follows the Prototype Pattern where you can register a globally singleton instance (as the prototype) for each service type then you will get an individual clone per scope/request. Modifying that clone will not affect the singleton instance as well as the subsequent clones. That is to say now you don’t need to clone a writer setting before editing it with the request-related information just feel safe to modify it for any specific request.The
AddDefaultODataServices
method registers a set of service types with default implementations that come from ODataLib. Typically you MUST call this metod first on your container builder before registering any custom service. Please note that the order of registration matters! ODataLib will always use the last service implementation registered for a specific service type.Currently the default services provided by ODataLib and expected to be overrided by users are:
Service Default Implementation Lifetime Prototype? IJsonReaderFactory DefaultJsonReaderFactory Singleton N IJsonWriterFactory DefaultJsonWriterFactory Singleton N ODataMediaTypeResolver ODataMediaTypeResolver Singleton N ODataMessageReaderSettings ODataMessageReaderSettings Scoped Y ODataMessageWriterSettings ODataMessageWriterSettings Scoped Y ODataPayloadValueConverter ODataPayloadValueConverter Singleton N IEdmModel EdmCoreModel.Instance Singleton N ODataUriResolver ODataUriResolver Singleton N UriPathParser UriPathParser Scoped N ODataSimplifiedOptions ODataSimplifiedOptions Scoped Y Build and Use the Container in ODataLib
After you have registered all the required services into the container builder, you can finally build a container from it by calling
BuildContainer
on your container builder. You will then get a container instance that implementsIServiceProvider
.In order for ODataLib to use the registered services, the container must be passed into ODataLib through some entry points. Currently entry points in ODataLib are
ODataMessageReader
,ODataMessageWriter
andODataUriParser
, which will be covered in the next two subsections.Part I: Serialization and Deserialization
The way of passing container into
ODataMessageReader
andODataMessageWriter
is exactly the same which is through request and response messages. We are still using the interfacesIODataRequestMessage
andIODataResponseMessage
but now the actual implementation class (e.g.,ODataMessageWrapper
in Web API OData) must also implementIContainerProvider
. Below is an excerpt of theODataMessageWrapper
class as an example if you are building an OData service directly using ODataLib.After that, the container will be stored in the
Container
properties ofODataMessageInfo
,ODataInputContext
andODataOutputContext
(and their subclasses). If you are implementing a custom media type (like Avro, VCard, etc.), you can access the container through those properties. This is a very advanced and complicated scenario thus we will omit the sample here for now.If you fail to set the
Container
inIContainerProvider
, it will remainnull
. In this case, ODataLib will not fail internally but all services will have their default implementations and there would be NO way to replace them with custom ones. That said, if you want extensibility, please use DI :-)Part II: URI Parsing
The way of passing container into URI parsers is a little bit different. You must use the constructor overloads (see below) of
ODataUriParser
that take a parametercontainer
ofIServiceProvider
to do so. Using the other constructors will otherwise disable the DI support in URI parsers.Then the container will be stored in
ODataUriParserConfiguration
and used in URI parsers. CurrentlyODataUriResolver
,UriPathParser
andODataSimplifiedOptions
can be overrided and will affect the behavior of URI parsers.Design ODataLib Features for DI
In the future, we may encounter the need in ODataLib to either move existing classes into DI container, or design new classes that work with DI. Based on the past experience about incorporating DI into ODataLib, here are some tips:
- Eliminate constructor parameters that are of primitive types because they CANNOT be injected. If they have to be there anyway, consider injecting a factory class instead of the class itself (e.g,
IJsonReader
andIJsonReaderFactory
). - Move those types (they are called dependencies) of the remaining constructor parameters into DI container (if they are not in it already) so that they can be injected by the DI framework automatically. If some types cannot be placed in DI container anyway, consider converting the constructors parameters of those types to class properties and using property assignment during initialization.
- Of course it’s best to use empty constructors.
- Carefully consider the lifetime of the service. We rarely use
Transient
as it will degrade the runtime performance of GC. If you want that service to have an individual instance per request, useScoped
. If only one instance is required during the application lifecycle, useSingleton
. Please also pay attention to the lifetime of your dependencies! - Add the service into
AddDefaultODataServices
of theContainerBuilderExtensions
class.
Adapt to Breaking Changes for DI
After upgrading to ODataLib v7.x, you might find that some parameters or properties in public APIs are missing. Don’t panic! Mostly you will find it in the list (see above) of the services registered in the container. And you will also find the request container in the context. Then it’s very easy to access the missing objects by calling
IServiceProvider.GetService
. Sometimes retrieving a service every time from the container might look like a performance concern though the actual cost of the DI framework is typically very low (for example, the MS DI Framework uses compiled lambda to optimize for performance). In this case, you might want to cache it in some place but please be cautious that inproper caching may break the lifetime policy of the services.
2. EDMLIB
-
2.1 Build a basic model
The EDM (Entity Data Model) library (abbr. EdmLib) primarily contains APIs to build an entity data model that conforms to CSDL (Common Schema Definition Language), and to read/write an entity data model from/to a CSDL document.
This section shows how to build a basic entity data model using EdmLib APIs.
Software used in this tutorial
Create a Visual Studio project
In Visual Studio, from the File menu, select New > Project.
Expand Installed > Templates > Visual C# > Windows Desktop, and select the Console Application template. Name the project EdmLibSample, and click OK.
Install the EdmLib package
From the Tools menu, select NuGet Package Manager > Package Manager Console. In the Package Manager Console window, type:
This command configures the solution to enable NuGet restore, and installs the latest EdmLib package.
Add the SampleModelBuilder class
The
SampleModelBuilder
class is used to build and return an entity data model instance at runtime.In Solution Explorer, right-click the project EdmLibSample. From the context menu, select Add > Class. Name the class SampleModelBuilder.
In the SampleModelBuilder.cs file, add the following
using
directives to introduce the EDM definitions:Then replace the boilerplate code with the following:
Add complex Type Address
In the SampleModelBuilder.cs file, add the following code into the
SampleModelBuilder
class:This code:
- Defines a keyless complex type
Address
within the namespaceSample.NS
; - Adds three structural properties
Street
,City
, andPostalCode
; - Adds the
Sample.NS.Address
type to the model.
Add an enumeration Type Category
In the SampleModelBuilder.cs file, add the following code into the
SampleModelBuilder
class:This code:
- Defines an enumeration type
Category
based onEdm.Int64
within the namespaceSample.NS
; - Sets the attribute
IsFlags
totrue
, so that multiple enumeration members (enumerators) can be selected simultaneously; - Adds three enumerators
Books
,Dresses
, andSports
; - Adds the
Sample.NS.Category
type to the model.
Add an entity type Customer
In the SampleModelBuilder.cs file, add the following code into the
SampleModelBuilder
class:This code:
- Defines an entity type
Customer
within the namespaceSample.NS
; - Adds a non-nullable property
Id
as the key of the entity type; - Adds a non-nullable property
Name
; - Adds a property
Credits
of the typeCollection(Edm.Int64)
; - Adds a nullable property
Interests
of the typeSample.NS.Category
; - Adds a non-nullable property
Address
of the typeSample.NS.Address
; - Adds the
Sample.NS.Customer
type to the model.
Add the default entity container
In the SampleModelBuilder.cs file, add the following code into the
SampleModelBuilder
class:This code:
- Defines an entity container
DefaultContainer
within the namespaceSample.NS
; - Adds the container to the model.
Note that each model MUST define exactly one entity container (aka. the default entity container) which can be referenced later via the
_model.EntityContainer
property.Add an entity set Customers
In the SampleModelBuilder.cs file, add the following code into the
SampleModelBuilder
class:This code directly adds a new entity set
Customers
to the default container.Using factory APIs for EdmModel
In the above sections, to construct entity/complex types and entity containers, one has to first explicitly instantiate the corresponding CLR types, and then invoke
EdmModel.AddElement()
to add them to the model. In this version, a new set of factory APIs are introduced that combine the two steps into one, leading to more succinct and cleaner user code. These APIs are defined as extension methods toEdmModel
as follows:So, for example, instead of
you could simply
Besides being more convenient, these factory APIs are potentially more efficient as advanced techniques may have been employed in their implementations for optimized object creation and handling.
Write the model to a CSDL document
Congratulations! You now have a working entity data model! In order to show the model in an intuitive way, we now write it to a CSDL document.
In the Program.cs file, add the following
using
directives:Then replace the boilerplate
Program
class with the following:For now, there is no need to understand how the model is written to CSDL. The details will be explained in the following section.
Run the sample
From the DEBUG menu, click Start Debugging to build and run the sample. The console window should appear and then disappear in a flash.
Open the csdl.xml file under the output directory with Internet Explorer (or any other XML viewer you prefer). The content should look similar to the following:
As you can see, the document contains all the elements we have built so far.
References
- Defines a keyless complex type
-
2.2 Read and write models
Models built with EdmLib APIs are in object representation, while CSDL documents are in XML representation. The conversion from models to CSDL is accomplished by the
CsdlWriter
APIs which are mostly used by OData services to expose metadata documents (CSDL). In contrast, the conversion from CSDL to models is done by theCsdlReader
APIs which are usually used by OData clients to read metadata documents from services.This section shows how to read and write entity data models using EdmLib APIs. We will use and extend the sample from the previous section.
Using the CsdlWriter APIs
We have already used one of the
CsdlWriter
APIs to write the model to a CSDL document in the previous section.The
CsdlWriter.TryWriteCsdl()
method is prototyped as:The second parameter
writer
requires anXmlWriter
which can be created through the overloadedXmlWriter.Create()
methods. Remember to either apply ausing
statement on anXmlWriter
instance or explicitly callXmlWriter.Flush()
(orXmlWriter.Close()
) to flush the buffer to its underlying stream. The third parametertarget
specifies the target implementation of the CSDL being generated, which can be eitherCsdlTarget.EntityFramework
orCsdlTarget.OData
. The 4th parametererrors
is used to pass out the errors encountered in writing the model. If the method returnstrue
(indicate success), theerrors
should be an emptyEnumerable
; otherwise it contains all the errors encountered.Using the CsdlReader APIs
The simplest
CsdlReader
API is prototyped as:The first parameter
reader
takes anXmlReader
that reads a CSDL document. The second parametermodel
passes out the parsed model. The third parametererrors
passes out the errors encountered in parsing the CSDL document. If the return value of this method istrue
(indicate success), theerrors
should be an emptyEnumerable
; otherwise it will contain all the errors encountered.Roundtrip the model
In the Program.cs file, insert the following code to the
Program
class:This code first reads the model from the CSDL document csdl.xml, and then writes the model to another CSDL document csdl1.xml.
Run the sample
Build and run the sample. Then open the file csdl.xml and the file csdl1.xml under the output directory. The content of csdl1.xml should look like the following:
You can see that the contents of csdl.xml and csdl1.xml are exactly the same except for the order of the elements. This is because EdmLib will reorder the elements when parsing a CSDL document.
References
-
2.3 Define entity relations
Entity relations are defined by navigation properties in entity data models. Adding a navigation property to an entity type using EdmLib APIs is as simple as adding a structural property shown in previous sections. EdmLib supports adding navigation properties targeting an entity set in the entity container or a contained entity set belonging to a navigation property.
This section shows how to define navigation properties using EdmLib APIs. We will use and extend the sample from the previous section.
Add a navigation property Friends
In the SampleModelBuilder.cs file, insert the following code into the
SampleModelBuilder
class:This code:
- Adds a navigation property
Friends
to the entity typeCustomer
; - Sets the
ContainsTarget
property tofalse
since this property has no contained entities but targets one or moreCustomer
entities in the entity setCustomers
; - Sets the
TargetMultiplicity
property toEdmMultiplicity.Many
, indicating that one customer can have many orders. Other possible values includeZeroOrOne
andOne
.
Add entity Type Order and entity set Orders
Just as how we added the entity set
Customers
, we first add an entity typeOrder
and then the entity setOrders
.In the SampleModelBuilder.cs file, insert the following code into the
SampleModelBuilder
class:In the Program.cs file, insert the following code into the
Main()
method:Add navigation properties Purchases and Intentions
In the SampleModelBuilder.cs file, insert the following code into the
SampleModelBuilder
class:This code:
- Adds a
Purchases
navigation property targeting one or more settled orders in the entity setOrders
; - Adds an
Intentions
navigation property targeting a contained entity set of unsettled orders that are not listed in the entity setOrders
.
Run the sample
Build and run the sample. Then open the file csdl.xml under the output directory. The content of it should look like the following:
References
- Adds a navigation property
-
2.4 Define singletons
Defining a singleton in the entity container shares the same simple way as defining an entity set.
This section shows how to define singletons using EdmLib APIs. We will use and extend the sample from the previous section.
Add a singleton VipCustomer
In the SampleModelBuilder.cs file, add the following code into the
SampleModelBuilder
class:This code directly adds a new singleton
VipCustomer
to the default container.In the Program.cs file, insert the following code into the
Main()
method:Run the Sample
Build and run the sample. Then open the file csdl.xml under the output directory. The content should look like the following:
References
[Tutorial & Sample] Use Singleton to define your special entity.
-
2.5 Define type inheritance
Type inheritance means defining a type by deriving from another type. EdmLib supports defining both derived entity types and derived complex types. Adding a derived entity (complex) type is almost identical to adding a normal entity (complex) type except that an additional base type needs to be specified.
This section shows how to define derived entity (complex) types using EdmLib APIs. We will use and extend the sample from the previous section.
Add derived entity type UrgentOrder
In the SampleModelBuilder.cs file, add the following code into the
SampleModelBuilder
class:Then in the Program.cs file, insert the following code into the
Main()
method:This code:
- Defines the derived entity type
UrgentOrder
within the namespaceSample.NS
, whose base type isSample.NS.Order
; - Adds a structural property
Deadline
of typeEdm.Date
; - Adds the derived entity type to the entity data model.
Add derived complex type WorkAddress
In the SampleModelBuilder.cs file, add the following code into the
SampleModelBuilder
class:Then in the Program.cs file, insert the following code into the
Main()
method:This code:
- Defines the derived complex type
WorkAddress
within the namespaceSample.NS
, whose base type isSample.NS.Address
; - Adds a structural property
Company
of typeEdm.String
; - Adds the derived complex type to the entity data model.
Run the sample
Build and run the sample. Then open the file csdl.xml under the output directory. The content should look like the following:
- Defines the derived entity type
-
2.6 Define operations
EdmLib supports defining all types of operations (actions and functions) and operation imports (action imports or function imports). Putting aide the conceptual differences between actions and functions, the way to define them could actually be shared between actions and functions.
This section shows how to define operations and operation imports using EdmLib APIs. We will use and extend the sample from the previous section.
Add bound action Rate
In the SampleModelBuilder.cs file, add the following code into the
SampleModelBuilder
class:Then in the Program.cs file, insert the following code into the
Main()
method:This code:
- Defines a bound action
Rate
within the namespaceSample.NS
, which has no return value; - Adds a binding parameter
customer
of typeSample.NS.Customer
; - Adds a parameter
rating
of typeEdm.Int32
; - Adds the bound action to the model.
Add an unbound function MostExpensive
In the SampleModelBuilder.cs file, add the following code into the
SampleModelBuilder
class:Then in the Program.cs file, insert the following code into the
Main()
method:This code:
- Defines an unbound parameterless composable function
MostExpensive
within the namespaceSample.NS
; - Adds the function to the model.
Add function import MostValuable
In the SampleModelBuilder.cs file, add the following code into the
SampleModelBuilder
class:And in the Program.cs file, insert the following code into the
Main()
method:This code:
- Directly adds a function import
MostValuable
to the default container; - Have the function import return a
Sample.NS.Order
entity from and only from the entity setOrders
.
The
Sample.NS.MostValuable
function import is actually theSample.NS.MostExpensive
function exposed in the entity container with a different name (could be any valid name).Run the sample
Build and run the sample. Then open the file csdl.xml under the output directory. The content should look like the following:
- Defines a bound action
-
2.7 Define annotations
EdmLib supports adding annotations on various model elements, including entity sets, entity types, properties, and so on. Annotations can be put under the
Annotations
XML element, or under the annotated target model elements (inline annotations). Users can specify the serialization location using the EdmLib API.This section shows how to define annotations using EdmLib APIs. We will use and extend the sample from the previous section.
Add an annotation to entity set Customers
In the SampleModelBuilder.cs file, add the following
using
directive:Then add the following code into the
SampleModelBuilder
class:And in the Program.cs file, insert the following code into the
Main()
method:This code adds an
Edm.Int32
annotationSample.NS.MaxCount
to the entity setCustomers
, which is put under theAnnotations
element.Add an inline Annotation to the Entity Type Customer
In the SampleModelBuilder.cs file, insert the following code into the
SampleModelBuilder.BuildAnnotations()
method:This code adds an inline
Edm.String
annotationSample.NS.KeyName
targetting the entity typeCustomer
.Add an inline annotation to the property Customer.Name
In the SampleModelBuilder.cs file, insert the following code into the
SampleModelBuilder.BuildAnnotations()
method:This code adds an inline
Edm.Int32
annotationSample.NS.Width
to the propertyCustomer.Name
.Run the sample
Build and run the sample. Then open the file csdl.xml under the output directory. The content should look like the following:
-
2.8 Using model utilities
The model utilities include many useful extension methods to various EDM classes and interfaces (e.g., IEdmModel, IEdmType, …). The extension methods are intended to implement some commonly reusable logic to simplify model manipulations. These methods can be roughly classified into five categories:
- Searching. The naming convention is
Find<ElementName>
(e.g.,IEdmModel.FindDeclaredEntitySet()
); - Predicate. The naming convention is
Is<ElementName>
(e.g.,IEdmOperation.IsFunction()
); - Information. The naming convention is
<InformationName>
(e.g.,IEdmNavigationSource.EntityType()
); - Getter. The naming convention is
Get<Name>
(e.g.,IEdmModel.GetTermValue()
); - Setter. The naming convention is
Set<Name>
(e.g.,IEdmModel.SetEdmVersion()
).
The most widely used parts are Searching, Predicate, and Information. Extension methods in the latter two parts are trivial, because they work literally as their names suggest. This section focuses on Searching. We will use and extend the sample from the previous section.
Exercise model utility APIs
In the Program.cs file, add the
using
directiveand insert the following code into the
Program
class:Run the sample
From the DEBUG menu, click Start Without Debugging to build and run the sample. The console window should not disappear after program exits.
The output on the console window should look like the following:
- Searching. The naming convention is
-
2.9 Model references
Model referencing is an advanced OData feature. When you want to use types defined in another model, you can reference that model in your own model. Typically when talking about model referencing, there is a main model and one or more sub-models. The main model references the sub-models. The role a particular model plays is not fixed, for a main model may also be referenced by another model. That is, models can be mutually referenced.
This section covers a scenario where we have one main model and two sub-models. The main model references the two sub-models, while the two sub-models references each other. We will introduce two ways to define model references: by code and by CSDL. If you would like to create the model by writing code, you can take a look at the first part of this section. If you want to create your model by reading a CSDL document, please refer to the second part.
Define model references by code
Let’s begin by defining the first sub-model
subModel1
. The model contains a complex typeNS1.Complex1
which contains a structural property of another complex type defined in another model. We also add a model reference tosubModel1
pointing to the second model located athttp://model2
. The URL should be the service metadata location. The namespace to include isNS2
and the model alias isAlias2
.Then we do the same thing for the second sub-model
subModel2
. This model contains a complex typeNS2.Complex2
and references the first model located athttp://model1
.Now we will add one structural property to the two complex types
NS1.Complex1
andNS2.Complex2
, respectively. The key point is that the property type is defined in the other model.After defining the two sub-models, we now define the main model that contains a complex type
NS.Complex3
and references the two sub-models. This complex type contains two structural properties of typeNS1.Complex1
andNS2.Complex2
, respectively.Define model references by CSDL
As an example, we store the CSDL of the three models in three string constants and create three
StringReader
s as if we are reading the model contents from remote locations.The model constructed in either way should be the same.
-
2.10 Define referential constraints
Referential constraints ensure that entities being referenced (principal entities) always exist. In OData, having one or more referential constraints defined for a partner navigation property on a dependent entity type also enables users to address the related dependent entities from principal entities using shortened key predicates (see [OData-URL]). A referential constraint in OData consists of one principal property (the ID property of the entity being referenced) and one dependent property (the ID property to reference another entity). This section shows how to define referential constraints on a partner navigation property.
Sample
Create an entity type
Test.Customer
with a key propertyid
of typeEdm.String
.Create an entity type
Test.Order
with a composite key consisting of two key propertiescustomerId
andorderId
both of typeEdm.String
.Customer.id
is the principal property whileOrder.customerId
is the dependent property. Create a navigation propertyorders
on the principal entity typeCustomer
.Then, create its corresponding partner navigation property on the dependent entity type
Order
with referential constraint.Create an entity type
Test.Detail
with a composite key consisting of three key propertiescustomerId
of typeEdm.String
,orderId
of typeEdm.String
, andid
of typeEdm.Int32
.Create an entity type
Test.DetailedOrder
which is a derived type ofTest.Order
. We will use this type to illustrate type casting in between multiple navigation properties.Come back to the type
Test.Detail
. There are two referential constraints here:DetailedOrder.orderId
is the principal property whileDetail.orderId
is the dependent property.DetailedOrder.customerId
is the principal property whileDetail.customerId
is the dependent property.
Create a navigation property
details
.Then, create its corresponding partner navigation property on the dependent entity type
Detail
with referential constraint.Please note that you should NOT specify
Customer.id
as the principal property because the association (represented by the navigation propertydetails
) is fromDetailedOrder
toDetail
rather than fromCustomer
toDetail
. And those properties must be specified in the order shown.Then you can query the
details
using either full key predicatehttp://host/customers('customerId')/orders(customerId='customerId',orderId='orderId')/Test.DetailedOrder/details(customerId='customerId',orderId='orderId',id=1)
or shortened key predicate
http://host/customers('customerId')/orders('orderId')/Test.DetailedOrder/details(1)
Key-as-segment convention is also supported
http://host/customers/customerId/orders/orderId/Test.DetailedOrder/details/1
-
2.11 Specify type facets for type definitions
Users can specify various type facets for references to type definitions. The only constraint is that the type facets specified should be applicable to the underlying type definition.
If you want to specify type facets, you need to call this constructor:
Here is sample code to create an EDM type definition reference with type facets
isNullable
:true
to allownull
values;false
otherwise.isUnbounded
:true
to indicateMaxLength="max"
;false
to indicate that theMaxLength
is a bounded value.maxLength
:null
for unspecified; other values for specified lengths. Invalid ifisUnbounded
istrue
.isUnicode
:true
if the encoding is Unicode;false
for non-Unicode encoding;null
for unspecified.precision
:null
for unspecified; other values for specified precisions; MUST be non-negative.scale
:null
to indicateScale="variable"
; other values for specified scales; MUST be non-negative.spatialReferenceIdentifier
:null
to indicateSRID="variable"
; other values for specified SRIDs; MUST be non-negative.
It’s worth mentioning that if you call the overloaded constructor taking two parameters, all the type facets will be auto-computed according to the OData protocol. For example, the default value of
SRID
is0
for geometry types and4326
for geography types. -
2.12 Other topics
References
[Tutorial & Sample] How to Use Open Type in OData.
[Tutorial & Sample] Using Unsigned Integers in OData.
3. SPATIAL
-
3.1 Define spatial properties
Using Spatial in OData services involves two parts of work:
- Define structural properties of spatial type in entity data models;
- Create and return spatial instances as property values in services.
This section shows how to define spatial properties in entity data models using EdmLib APIs. We will continue to use and extend the sample from the EdmLib sections.
Add properties GeometryLoc and GeographyLoc
In the SampleModelBuilder.cs file, insert the following code into the
SampleModelBuilder.BuildAddressType()
method:This code:
- Adds a default
Edm.GeometryPoint
propertyGeometryLoc
to theAddress
type; - Adds an
Edm.GeographyPoint
propertyGeographyLoc
with a type facetSrid=1234
to theAddress
type.
Run the sample
Build and run the sample. Then open the csdl.xml file under the output directory. The content of csdl.xml should look like the following:
-
3.2 Create spatial instances
This section shows how to create spatial instances using Spatial APIs and return them as property values of OData entries.
Create GeometryPoint and GeographyPoint instances
In order to use spatial types, please add the following
using
directive:The following code shows how to create
GeometryPoint
andGeographyPoint
instances:Spatial instances can be directly put into
ODataPrimitiveValue
as property values. Using theAddress
type from the last section:An
ODataResource
for theAddress
type could be constructed as follows:Construct more complex spatial instances
Directly creating these instances using Spatial APIs would be a bit complicated. So we highly recommend that you download and add the SpatialFactory.cs file to your project and use the
GeometryFactory
or theGeographyFactory
class to construct more complex spatial instances.Here are some sample code of how to use the factory classes to create spatial instances:
More samples could be found in the test cases of the
Microsoft.Spatial.Tests
project. Please find the source code here.References
4. RELEASE NOTES
-
ODataLib 7.0.0
To briefly summarize the breaking changes, most of them fall into one of four categories:
Improved Performance
We will get better writer performance across the board.
Introducing Dependency Injection
This feature will substantially increase extensibility, like allowing customers to replace entire components such as the
UriPathParser
with their own implementation. Introducing DI make it much easier to use the same reader/writer settings across the board.Removed Legacy Code
There was a lot of vestigial code left around from the OData v1-3 days that we’ve removed.
Improved API Design
Most of our API improvements fall into the category of namespace simplifications or updating verbiage. The single most impactful change that we made was deciding to merge entity type and complex type in ODataLib. We did this because complex type and entity type are becoming more and more similar in the protocol, but we continue to pay overhead to make things work for both of them.
Changes in ODataLib 7.0 Release
New Features
[Issue #245] Support duplicate non-OData query options.
[Issue #248] Support untyped JSON.
- This feature will allow customers to extend their OData payloads with arbitrary JSON. In the extreme, it’s theoretically possible for the whole response to be untyped.
[Issue #271] Support writing relative URIs in OData batch operations.
- Add an enum type
BatchPayloadUriOption
to support three URI formats in batch payload.
[Issue #366] Support collection of mixed primitive types and Edm.Untyped.
[Issue #501] Integrate dependency injection (DI).
-
We introduced an abstraction layer consisting of two interfaces
IContainerBuilder
andIContainerProvider
so that ODataLib is decoupled from any concrete implementation of DI framework. Now we support the following services to be replaced by users:- JSON reader via
IJsonReaderFactory
- JSON writer via
IJsonWriterFactory
ODataMediaTypeResolver
ODataPayloadValueConverter
ODataUriResolver
UriPathParser
- JSON reader via
-
We also support prototype services. For each prototype service, you can specify a globally singleton prototype instance. Then for each request, a cloned instance will be created from that prototype which can isolate the modification within the request boundary. Currently we support three prototype services:
ODataMessageReaderSettings
ODataMessageWriterSettings
ODataSimplifiedOptions
[Issue #502] Support URI path syntax customization.
- Expose
UriPathParser.ParsePathIntoSegments
to support customizing how to separate a Uri into segments - Provide
ParseDynamicPathSegmentFunc
for customizing how to parse a dynamic path segment.
[Issue #613] Support type facets when referencing
TypeDefinition
types.[Issue #622] Support navigation property on complex types.
- EdmLib supports adding navigation property on complex type in model.
- ODataUriParser support parsing related Uri path or query expressions.
- ODataLib support reading and writing navigation properties on complex type.
[Issue #629] Support multi-NavigationPropertyBindings for a single navigation property by using different paths
- Navigation property used in multi bindings with different path is supported for navigation under containment and complex.
[Issue #631] Support fluent writer API.
- In previous version, paired
WriteStart
andWriteEnd
calls are used in writing payloads. This syntax is error-prone, and soon gets unmanageable with complex and deeply nested payloads. In this new release, you can instead write payloads using the neat fluent syntax.
[Issue #635] Support collection of nullable values for dynamic properties.
[Issue #637] Support system query options without $ prefix.
- Expose
ODataUriParser.EnableNoDollarQueryOptions
flag to enable user to support ‘$’ system query prefix optional.
Add
ODataSimplifiedOptions
class for simplified reader, writer, URL parsing options.Support duplicate custom instance annotations.
Fixed Bugs
[Issue #104] Function imports with parameters are included in service document.
[Issue #498] Typos in namespace/class names.
[Issue #508] YYYY-MM-DD in URI should be parsed into
Date
, notDataTimeOffset
.[Issue #556] URI template parser doesn’t work correctly if key is of an enum type.
[Issue #573]
CollectionCount
is not publicly accessible.[Issue #592]
ODataUriParser.ParsePath()
doesn’t work correctly ifEntitySetPath
is not specified in model.[Issue #628] Dynamic complex (collection) property is annotated with association and navigation links.
[Issue #638] Null value at the first position in a complex collection cannot be read.
[Issue #658]
ODataValueUtils.ToODataValue
doesn’t work withSystem.Enum
objects.Improvements
Legacy Code Clean-up
[Issue #385] Remove junk code to improve stability and reduce assembly size.
- Deprecated build constants
- Code in ODataLib that has duplication in EdmLib
- Platform-dependent code (e.g., redefinition of TypeCode and BindingFlags, Silverlight code, etc.)
- Deprecated classes like
EntitySetNode
[Issue #500] Remove deprecated NuGet profiles
- Removed support for:
- Desktop and Profile328 for .NET 4.0
- Profile259 for .NET 4.5
- dnxcore50 and dnx451 for ASP.NET 5.0 (deprecated)
- What we have now:
- Profile111 for .NET 4.5 which is compatible with most .NET 4.5+ applications, UWP, Xamarin/Mono, etc.
- .NET 3.5 for Office (will not be publicly available)
[Issue #510] Remove Atom support
[Issue #511] Clean property and method in
ODataMessageReaderSettings
andODataMessageWriterSettings
- Remove API to enable default, service and client settings
- Move
ODataSimplified
andUseKeyAsSegment
toODataSimplifiedOptions
- Rename DisableXXX to EnableXXX
- Remove base class of reader and writer settings
[Issue #548] Rename “value term” and “value annotation” in EdmLib.
- “Value term” and “value annotation” are concepts of ODataV3, In ODataV4 we remove/rename obsolete interfaces and merge some interfaces with their base interfaces so as to make APIs clearer and more compact.
[Issue #565] Update CoreVocabularies.xml to the new version and related APIs.
[Issue #564] Remove
Edm.ConcurrencyMode
attribute from Property.[Issue #606] Remove V3 vocabulary expressions
- Interfaces removed:
IEdmEnumMemberReferenceExpression
,IEdmEnumMemberReferenceExpression
,IEdmEntitySetReferenceExpression
,IEdmPropertyReferenceExpression
,IEdmParameterReferenceExpression
andIEdmOperationReferenceExpression
- For the previous
IEdmEntitySetReferenceExpression
, please useIEdmPathExpression
where users need to provide a path (a list of strings) to a navigation property with which we can resolve the target entity set from the navigation source. - For the previous
IEdmOperationReferenceExpression
, please useIEdmFunction
because theEdm.Apply
only accepts an EDM function in OData V4. This also simplifies the structure ofEdm.Apply
expression.
[Issue #618] Remove deprecated validation rules
ComplexTypeInvalidAbstractComplexType
ComplexTypeInvalidPolymorphicComplexType
ComplexTypeMustContainProperties
OnlyEntityTypesCanBeOpen
Public API Simplification
[Issue #504] Unify entity and complex (collection) type serialization/deserialization API.
- Merge the reading/writing behavior for complex and entity, collection of complex and collection of entity.
- Change
ODataEntry
toODataResource
and removeODataComplexValue
to support complex and entity. - Change
ODataFeed
toODataResourceSet
to support feed and complex collection. - Change
ODataNavigationLink
toODataNestedResourceInfo
to support navigation property and complex property or complex collection property. - Change reader/writer APIs to support
IEdmStructuredType
.
- Change
- We don’t merge entity type/complex type in EdmLib since they are OData concepts.
[Issue #491] Simplified namespaces.
[Issue #517] Centralized reader/writer validation. [Breaking Changes]
- Add an enum
ValidationKinds
to represent all validation kinds in reader and writer. - Add Validations property in
ODataMessageWriterSettings
/ODataMessageReaderSettings
to control validations. - Remove some APIs.
[Issue #571] Rename
ODataUrlConvention
toODataUrlKeyDelimiter
- Rename
ODataUrlConvention
toODataUrlKeyDelimiter
. - Use
ODataUrlKeyDelimiter.Slash
instead ofODataUrlConvention.Simplified
orODataUrlConvention.KeyAsSegment
- Use
ODataUrlKeyDelimiter.Parentheses
instead ofODataUrlConvention.Default
[Issue #614] Improve API design around
ODataAnnotatable
- Merge
SerializationTypeNameAnnotation
intoODataTypeAnnotation
- Refactor
ODataTypeAnnotation
to contain only the type name - Remove unnecessary inheritance from
ODataAnnotatable
in URI parser and simplify the API ofODataAnnotatble
GetAnnotation<T>()
andSetAnnotation<T>()
will no longer be available becauseODataAnnotatable
should only be responsible for managing instance annotations.
Public API Enhancement
[Issue #484] Preference header extensibility
- Public class
HttpHeaderValueElement
, which represents http header value element - Remove sealed from public class
ODataPreferenceHeader
[Issue #544] Change Enum member value type from
IEdmPrimitiveValue
to a more specific type.- Add interface
IEdmEnumMemberValue
and classEdmEnumMemberValue
to represent enum member value specifically.AddMember()
underEnumType
now acceptsIEdmEnumMemberValue
instead ofIEdmPrimitiveValue
as member value.
[Issue #621] Make reader able to read contained entity/entityset without context URL.
[Issue #640] More sensible type, namely IEnumerable
[Issue #643] Adjust query node kinds in Uri Parser in order to support navigation under complex. [Breaking Changes]
Improved standard-compliance by forbidding duplicate property names.
Writer throws more accurate and descriptive exceptions.
Other Improvements
[Issue #493] Replace
ThrowOnUndeclaredProperty
with the more accurateThrowOnUndeclaredPropertyForNonOpenType
.[Issue #551] Change the type of
EdmReference.Uri
toSystem.Uri
.[Issue #558, [Issue #611] Improve writer performance. Up to 25% improvements compared to ODL 6.15 are achieved depending on scenario.
[Issue #632] Rename CsdlXXX to SchemaXXX, and EdmxXXX to CsdlXXX.
The original naming is confusing. According to the CSDL spec:
An XML document using these namespaces and having an edmx:Edmx root element will be called a CSDL document.
So, Edmx is only part of a valid CSDL document. In previous version,
CsdlReader
is actually unable to read a CSDL document. It’s only able to read the Schema part of it.EdmxReader
, on the other hand, is able to read a whole CSDL document. To clear up the concepts, the following renaming has been done:CsdlReader/Writer
toSchemaReader/Writer
;EdmxReader/Writer
toCsdlReader/Writer
;EdmxReaderSettings
toCsdlReaderSettings
;EdmxTarget
toCsdlTarget
.
Notes
This release delivers OData core libraries including ODataLib, EdmLib and Spatial. OData Client for .NET is not published in this release.
-
Breaking changes about Query Nodes
The expression of
$filter
and$orderby
will be parsed to multiple query nodes. Each node has particular representation, for example, a navigation property access will be interpreted asSingleNavigationNode
and collection of navigation property access will be interpreted asCollectionNavigationNode
.Since we have merged complex type and entity type in OData Core lib, complex have more similarity with entity other than primitive property. Also in order to support navigation property under complex, the query nodes’ type and hierarchy are changed and adjusted to make it more reasonable.
Nodes Change
Nodes Added
SingleComplexNode CollectionComplexNode
Nodes Renamed
Old New NonentityRangeVariable NonResourceRangeVariable EntityRangeVariable ResourceRangeVariable NonentityRangeVariableReferenceNode NonResourceRangeVariableReferenceNode EntityRangeVariableReferenceNode ResourceRangeVariableReferenceNode EntityCollectionCastNode CollectionResourceCastNode EntityCollectionFunctionCallNode CollectionResourceFunctionCallNode SingleEntityCastNode SingleResourceCastNode SingleEntityFunctionCallNode SingleResourceFunctionCallNode Nodes Removed
CollectionPropertyCast SingleValueCast
API Change
-
Add SingleResourceNode as the base class of SingleEntityNode and SingleComplexNode
-
SingleNavigationNode and CollectionNavigationNode accepts SingleResourceNode as parent node and also accepts bindingpath in the constructor.The parameter order is also adjusted.
Take SingleNavigationNode for example:
Changed to:
Behavior Change for complex type nodes
Complex property used to share nodes with primitive property, now it shares most nodes with entity. Here lists the nodes that complex used before and now.
Before Now NonentityRangeVariable ResourceRangeVariable NonentityRangeVariableReference ResourceRangeVariableReference SingleValuePropertyAccessNode SingleComplexNode SingleValueCastNode SingleResourceCastNode SingleValueFunctionCallNode SingleResourceFunctionCallNode CollectionPropertyAccessNode CollectionComplexNode CollectionPropertyCastNode CollectionResourceCastNode CollectionFunctionCallNode CollectionResourceFunctionCallNode -
-
Breaking changes about validation settings
We used to have lots of validation related members/flags in
ODataMessageReaderSettings
andODataMessageWriterSettings
. In OData 7.0, we cleaned up the out-dated flags and put the remained flags together and keep them be considered in a consistent way.Removed APIs
ODataMessageWriterSettings ODataMessageReaderSettings EnableFullValidation EnableFullValidation EnableDefaultBehavior() EnableDefaultBehavior() EnableODataServerBehavior() EnableODataServerBehavior() EnableWcfDataServicesClientBehavior() EnableWcfDataServicesClientBehavior() UndeclaredPropertyBehaviorKinds DisablePrimitiveTypeConversion DisableStrictMetadataValidation The EnablexxxBehavior() in writer and reader settings actually wrapped few flags.
ODataWriterBehavior ODataReaderBehavior AllowDuplicatePropertyNames AllowDuplicatePropertyNames AllowNullValuesForNonNullablePrimitiveTypes Those flags are all removed, and an enum type would represent all the settings instead.
New API
A flag enum type ValidationKinds to represent all validation kinds in reader and writer:
Writer: Add member Validations which accepts all the combinations of ValidationKinds
Reader: Add member Validations which accepts all the combinations of ValidationKinds
Sample Usage
writerSettings.Validations = ValidationKinds.All
Equal to:writerSettings.EnableFullValidation = true
readerSettings.Validations |= ValidationKinds.ThrowIfTypeConflictsWithMetadata
Equal to:readerSettings.DisableStrictMetadataValidation = false
Same for reader.
-
Breaking changes about merge entity and complex
This page will describes the Public API changes for “Merge entity and complex”. The basic idea is that we named both an entity and a complex instance as an
ODataResource
, and named a collection of entity or a collection of complex as anODataResourceSet
.API Changes
Following is difference of public Apis between ODataLib 7.0 and ODataLib 6.15.
ODataLib 6.15 ODataLib 7.0 ODataEntry ODataResource ODataComplexValue ODataResource ODataFeed ODataResourceSet ODataCollectionValue for Complex ODataResourceSet ODataNavigationLink ODataNestedResourceInfo ODataPayloadKind Entry Resource Feed ResourceSet ODataReaderState EntryStart ResourceStart EntryEnd ResourceEnd FeedStart ResourceSetStart FeedEnd ResourceSetEnd NavigationLinkStart NestedResourceInfoStart NavigationLinkEnd NestedResourceInfoEnd ODataParameterReaderState Entry Resource Feed ResourceSet ODataInputContext ODataReader CreateEntryReader (IEdmNavigationSource navigationSource, IEdmEntityType expectedEntityType) ODataReader CreateResourceReader(IEdmNavigationSource navigationSource, IEdmStructuredType expectedResourceType) ODataReader CreateFeedReader (IEdmEntitySetBase entitySet, IEdmEntityType expectedBaseEntityType) ODataReader CreateResourceSetReader(IEdmEntitySetBase entitySet, IEdmStructuredType expectedResourceType) ODataOutputContext ODataWriter CreateODataEntryWriter (IEdmNavigationSource navigationSource, IEdmEntityType entityType) ODataWriter CreateODataResourceWriter(IEdmNavigationSource navigationSource, IEdmStructuredType resourceType) ODataWriter CreateODataFeedWriter (IEdmEntitySetBase entitySet, IEdmEntityType entityType) ODataWriter CreateODataResourceSetWriter(IEdmEntitySetBase entitySet, IEdmStructuredType resourceType) ODataParameterReader ODataReader CreateEntryReader () ODataReader CreateResourceReader () ODataReader CreateFeedReader () ODataReader CreateResourceSetReader () ODataParameterWriter ODataWriter CreateEntryWriter (string parameterName) ODataWriter CreateResourceWriter (string parameterName) ODataWriter CreateFeedWriter (string parameterName) ODataWriter CreateResourceSetWriter (string parameterName) ODataMessageReader public Microsoft.OData.Core.ODataReader CreateODataEntryReader () public Microsoft.OData.ODataReader CreateODataResourceReader () public Microsoft.OData.Core.ODataReader CreateODataEntryReader (IEdmEntityType entityType) public Microsoft.OData.ODataReader CreateODataResourceReader (IEdmStructuredType resourceType) public Microsoft.OData.Core.ODataReader CreateODataEntryReader (IEdmNavigationSource navigationSource, IEdmEntityType entityType) public Microsoft.OData.ODataReader CreateODataResourceReader (IEdmNavigationSource navigationSource, IEdmStructuredType resourceType) public Microsoft.OData.Core.ODataReader CreateODataFeedReader () public Microsoft.OData.ODataReader CreateODataResourceSetReader () public Microsoft.OData.Core.ODataReader CreateODataFeedReader (IEdmEntityType expectedBaseEntityType) public Microsoft.OData.ODataReader CreateODataResourceSetReader (IEdmStructuredType expectedResourceType) public Microsoft.OData.Core.ODataReader CreateODataFeedReader (IEdmEntitySetBase entitySet, IEdmEntityType expectedBaseEntityType public Microsoft.OData.ODataReader CreateODataResourceSetReader (IEdmEntitySetBase entitySet, IEdmStructuredType expectedResourceType) public ODataReader CreateODataUriParameterResourceSetReader(IEdmEntitySetBase entitySet, IEdmStructuredType expectedResourceType) public ODataReader CreateODataUriParameterResourceReader(IEdmNavigationSource navigationSource, IEdmStructuredType expectedResourceType) ODataMessageWriter public Microsoft.OData.Core.ODataWriter CreateODataEntryWriter () public Microsoft.OData.ODataWriter CreateODataResourceSetWriter () public Microsoft.OData.Core.ODataWriter CreateODataEntryWriter (IEdmNavigationSource navigationSource) public Microsoft.OData.ODataWriter CreateODataResourceSetWriter (IEdmEntitySetBase entitySet) public Microsoft.OData.Core.ODataWriter CreateODataEntryWriter (IEdmNavigationSource navigationSource, IEdmEntityType entityType) public Microsoft.OData.ODataWriter CreateODataResourceSetWriter (IEdmEntitySetBase entitySet, IEdmStructuredType resourceType) public Microsoft.OData.Core.ODataWriter CreateODataFeedWriter () public Microsoft.OData.ODataWriter CreateODataResourceWriter () public Microsoft.OData.Core.ODataWriter CreateODataFeedWriter (IEdmEntitySetBase entitySet) public Microsoft.OData.ODataWriter CreateODataResourceWriter (IEdmNavigationSource navigationSource) public Microsoft.OData.Core.ODataWriter CreateODataFeedWriter (IEdmEntitySetBase entitySet, IEdmEntityType entityType) public Microsoft.OData.ODataWriter CreateODataResourceWriter (IEdmNavigationSource navigationSource, IEdmStructuredType resourceType) public ODataWriter CreateODataUriParameterResourceWriter(IEdmNavigationSource navigationSource, IEdmStructuredType resourceType) public ODataWriter CreateODataUriParameterResourceSetWriter(IEdmEntitySetBase entitySetBase, IEdmStructuredType resourceType) -
ODataLib 7.1.0
Changes in ODataLib 7.1.0 Release
Migration to .NET Standard 1.1
[Commit a2af8c6c19f104f46b442df7f57f624ed77d82fc] Update profile111 to .netstandard1.1.
[Commit 6f746cd0cd9d62a5bc57e345a910a6f9d8d4dc1b] Adding new projects and solution for .NET Standard version of ODL.
Creating a new solution file for .NET Standard projects. Adding .NET Standard versions of Microsoft.Spatial, Microsoft.OData.Edm, Microsoft.OData.Core, and Microsoft.OData.Client. MSBuild doesn’t pre-process the dependency graph provided by the csproj files, so explicitly spoonfeeding the chain in the Microsoft.Test.OData.DotNetStandard.sln file.
Update build script to default to VS 2015 for .NET Standard, removing NuGetPackage test project and deprecating it, adding E2E .NET Standard project
Remove duplicate file: IEdmReferentialConstraint.cs.
Change version to 7.1.0
Note: “nuget restore” needs to be run manually on the new project files for them to compile.
New Features
[Commit c0c6006a5a8683507c38623144a145de128851c6] Adding support and tests for virtual property count.
[Commit d064a1c6358130c581bb9d5bb32e774f8e921992] Adding support and tests for custom aggregation methods.
[Commit 8e965e9f89951dbfea510e67cbbe89e4f75fa69b] Add support for operations with no bindings.
Fixed Bugs
[Issue #525] Support for AnnotationPath.
[Issue #526] Support for IncludeInServiceDocument.
[Issue #680] Text “Date” will be parsed as “EDM.Date” type segment in URL parser.
[Issue #687] Null exception when HttpHeaderValueElement.Value is not set.
[Issue #706] Serialization exception when 2 subtypes define a property with the same name but different types.
[Issue #758] countdistinct and $count are returning Edm.Int64 which is not spec compliant.
[Issue #776] Fix ContextUrl generation for operations in path.
[Issue #777] Fix trailing whitespace on empty line.
[Issue #778] Fix tests and code for reading nested results from operations.
Improvements
[Commit 6e2dee52b37e620926cd0535f40d5537ba839c05] Add Test solutions for WP WindowsStore Portable.
[Commit d695d6ded6d44fa3fdb7abbd5f8dc19c29330e10] Update license.
[Commit 8521d38405351f789134d8945a696a18eec929d3] Fix Phone Project.
[Commit dc9632b686e47dba8a0bbeffb5cc8c5850e27c8b] Fix fxcop issue.
[Commit 3ea2d70d14c97344f43383d867a9edd81eb407a3] Add test cases for DotNetCore.
[Commit 2ef3fc921ad4b557ef67cd17ce95eb08b33fa14e] Update nuget.exe to 3.5.0 to resolve build issue with xunit.
[Commit 0dc70678f05201ebc48c83219f6b4450078d0693] Reordering comments to avoid compiler warnings.
[Commit bbd9ede1e73de3538af6f1a223bcc7fe689688f4] Update test project to copy the new and old version of the EntityFramework.nuspec file.
[Commit 6eb8a4b5ecff1d83d883a95162d187ee3a44f935] Remove use of StringBuilder.Clear() for .NET 3.5 support.
[Commit c1edd714bf58ddd2876e10d31f0bd9ad81e25664] Added metadata to tests for new bound function.
Notes
This release delivers OData core libraries including ODataLib, EdmLib and Spatial. OData Client for .NET is not published in this release.
-
ODataLib 7.1.1
Changes in ODataLib 7.1.1 Release
Notes
7.1.1 is a re-release of 7.1.0 without the NetStandard version of ODataLib, EdmLib and Spatial due to an issue found in the NetStandard versions of those binaries. The PCL versions of those binaries are unaffected.
This release delivers OData core libraries including ODataLib, EdmLib and Spatial. OData Client for .NET is not published in this release.
-
ODataLib 7.2.0
Changes in ODataLib 7.2.0 Release
Notes
7.2.0 re-introduces .NET Standard 1.1 libraries of ODataLib (OData.Core, OData.Edm, and Microsoft.Spatial). The PCL versions remain in the packages and are shipped alongside the new .NET Standard libraries. Bug fixes and additional test validations are also included in this release.
Features
[Commit 0b54111ee7909e71263b83fc60268de0de817986] Expose UriQueryExpressionParser.ParseFilter as a public API [#805]
Fixed Bugs
[Issue #789] BUG? Exception when create EdmModel V7.1.0
Improvements
[Commit 072f6f7c9bc4c739e553f7fa0996618c621a6589] Adding FxCop exclusion for CA3053:UseXmlSecureResolver in code that compiles under .Net portable framework.
[Commit 272c74afd1dda7a4a8e562c45dd9da9a9d74dd8b] Fix suppression for CA3053 to reference proper category and checkid.
[Commit 170827a01f9141649a848aefb13531163dedd65e] This change fixes test failures in both local machine and in the lab
[Commit 66738741dbecad8e0dc32fa787a9f98898a2beda] Adding .NET Core unit tests that integrate .NET Standard Libraries (#803)
This release delivers OData core libraries including ODataLib, EdmLib and Spatial. OData Client for .NET is not published in this release.
-
ODataLib 7.3.0
Changes in ODataLib 7.3.0 Release
Notes
ODataLib 7.3.0 adds support key features including optional function parameters, parser support for the $compute clause, primitive type casts, and the ability to read and write untyped data as structured values.
Features
[[#760] Aggregation not supported for dynamic properties
[#782] Add support for optional parameters in function
[#799] Support for $compute in $select and $filter
[#800] Support for structured reading/writing of untyped values
[#801] Supporting Primitive Type Casts
Fixed Bugs
[Issue #747] Cannot select dynamic property of a dynamic property
[Issue #814] Support writing enum-valued Annotations
[Issue #856] Raw Value serializer output json string for Spatial type
This release delivers OData core libraries including ODataLib, EdmLib and Spatial. OData Client for .NET is not published in this release.
-
ODataLib 7.3.1
Changes in ODataLib 7.3.1 Release
Notes
ODataLib 7.3.1 addresses an issue where $compute parsing was not triggered by a global call to ParseUri.
Fixed Bugs
[Issue #927] Add Compute to ParseUri.
This release delivers OData core libraries including ODataLib, EdmLib and Spatial. OData Client for .NET is not published in this release.
-
ODataLib 7.4.0
Changes in ODataLib 7.4.0 Release
Notes
ODataLib 7.4.0 has the following beta releases:
ODataLib 7.4.0.beta
Features
[Issue #103] OData v4: Deserialize client unknown properties into OData Object Model.
[Issue #801] Support Primitive Type Casts.
[Issue #988] Add support for 4.01 Delta Format
Fixed Bugs
[Issue #698] DataServiceQuerySingle
.GetValueAsync inconsistent with GetValue in support for GET returning 404. [Issue #800] Need for parsing Open types using OData.NET v7.X.
[Issue #949] ODataUriExtensions.BuildUri ignores $skiptoken.
[PR #961] Add support for ENUM keys.
[Issue #965] Parsing encoded character with special meaning in ODataJsonLightContextUriParser.
ODataLib 7.4.0.beta2
Features
[Issue #226] Support a json serialization for $batch.
[Issue #866] “Microsoft.OData.Client” support for ,NET Core.
Fixed Bugs
[PR #980] Support enum to string comparision.
[PR #995] Use ConcurrentDictionary in all platforms to make client edm model thread safe.
[Issue #1008 & Issue #1009] Fix property validation issue.
[Issue #1101] Microsoft.OData.Client 7.0+ needs to support GetValue like the older OData Client.
ODataLib 7.4.0.beta3
Features
[PR #1020] DependsOn Ids for Multipart/Mixed Batch
Fixed Bugs
[Issue #1022] Calculate correct context URI with Operation path segment.
[Issue #1193] Add the extension function back to derived class.
[Issue #1028] Add the enum member expression into validation and don’t return the error message.
ODataLib 7.4.0 RTM
Features
[Issue #1037] Support reading/writing OData 4.01 compatible JSON payloads.
This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.
-
ODataLib 7.4.1
Changes in ODataLib 7.4.1 Release
Notes
ODataLib 7.4.1 includes the following items: a new OData Client Code Gen extension for VS2017 using the latest version of the libraries, built-in abstract types for Edm models, KeyAsSegmentSupported boolean to the capabilities vocabulary, added validation rules to abstract types, support for AnnotationSegment, NuGet package testing, and various bug fixes.
Features
[[#987] Adding new OData Client Code Gen for VS2017
[[#1042] Remove the NavigationPropertyEntityMustNotIndirectlyContainItself rule
[[#1051] Add the build-in abstract type into Edm core model - Edm Type Part.
[[#1055] OptionalDollarSign: Small test update and expose API for DI option setter/getter
[[#1056] Add KeyAsSegmentSupported annotation term to Capabiliites vocabulary
[[#1058] Add the validation rules to the abstract types
[[#1075] Add support for AnnotationSegment to PathSegmentHandler.
[[#1080] Add nuget package testing.
Fixed Bugs
[Issue #530] LINQ query generation with Date functions produces weird urls
[Issue #1027] Edm.NavigationPropertyPath not supported
[Issue #1040] Need to update batch changeset ID to boundary value
[Issue #1046] Odata Edm lib issue with vocabulary
This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.
-
ODataLib 7.4.2
Changes in ODataLib 7.4.2 Release
Notes
ODataLib 7.4.2 includes the following items: support for “Authorization” vocabularies to align with the Open API specification, enabling support for containment paths in navigation property bindings by addressing a bug, and various other fixes.
Features
[#1070] Add the Authorization vocabularies annotation into core edm model
[#1109] Fix support for containment paths in nav prop bindings
[#1112] Bug fix: Throw exception for an invalid enum value
Fixed Bugs
[Issue #645] Enable updating top-level properties to null.
[Issue #1045] ODataUriExtensions.BuildUri ignores $apply
[Issue #1084] Updating package ID to match the existing VSIX ID in the marketplace
[Issue #1085] ExpressionLexer fix for parameter alias token in dotted expression
[Issue #1092] Address StyleCop warnings
This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.
-
ODataLib 7.4.3
Changes in ODataLib 7.4.3 Release
Notes
ODataLib 7.4.3 fixes a minor bug introduced in 7.4.2 in which the path for a contained entity set was computed incorrectly.
Fixed Bugs
[Issue #1121] Incorrect path calculated for contained entity sets. [Issue #1086] Enable writing type annotations for collections in full metadata. —
This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.
-
ODataLib 7.4.4
Changes in ODataLib 7.4.4 Release
Notes
ODataLib 7.4.4 fixes a potential concurrency issue with an internal dictionary used for tracking navigation property mappings and adds code to make sure that navigation property bindings are never written for containment navigation properties.
Fixed Bugs
[Issue #137] Possible contention issues in navigationPropertyMappings dictionary. [Issue #138] Containment navigation properties shouldn’t define NavigationPropertyBindings to non-containment sets
This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.
-
ODataLib 7.5.0
Changes in ODataLib 7.5.0 Release
Notes
ODataLib 7.5.0 includes the following items: IN operator, Entity set aggregation, various bug fixes, and performance improvement.
Features
[#757] Entity set aggregations
[#1165] [Feature] IN operator
Fixed Bugs
[#513] $select with complexCol/prop should be supported
[#985] VS 2017 15.4.2 Broken client
[#1043] Thread safety of context item collections
[#1044] Code generation from T4 template - IgnoreUnexpectedElementsAndAttributes doesn’t seem to work
[#1087] Batch is broken by load balancer (proxy)
[#1148] OData Code Generator does not install on VS2015
[#1123] Include dots in parseIdentifier for annotation parsing
[#1130] Add StringComparison in String.Equal() method call
[#1142] fix write document metadata properties for apply ComputeTransformationNode
[#1143] fix build uri from groupby navigation property with many child structural properties
[#1145] Memory Leak in Library?
[#1151] Port client-side DateTime support fix to 7.x
[#1153] UriParser should throw exception for mismatched keys count when UnqualifiedODataUriResolver is configured
[#1155] FunctionCallBinder issue when case-insensitive is enabled
[#1157] ODataMessageReader doesn’t honor the absence of ValidationKinds.ThrowIfTypeConflictsWithMetadata flag
[#1159] remove the Document element from Edm lib
[#1182] Fix a wrong if condition
[#1191] Avoid String allocations when writing strings that require special character handling
This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.
-
ODataLib 7.5.1
Changes in ODataLib 7.5.1 Release
Notes
ODataLib 7.5.1 includes the following bug fixes:
Fixed Bugs
[#1181] Refactor the JSON string escape in JSON writer
[#1216] Read parameter should not throw exception if missing optional parameter
[#1220] Combined length of user strings exceeds allowed limit in OData V4 Code Generator
[#1226] Cache the Edm full name for the Edm types
[#1232] Fix lexer bug that breaks functions named ‘in’
This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.
-
ODataLib 7.5.2
Changes in ODataLib 7.5.2 Release
Notes
ODataLib 7.5.2 includes the following new features, bug fixes and improvements:
Changes
[#1272] Support $index query option
[#1275] Support $apply in $expand
[#855] Support OptionalParameter as an external annotation
[#1262] OutOfLine Annotation for EnumMember throw exception when write to CSDL
[#1276] OData nuget package description should change to reflect the latest status
[#1287] Add Validation vocabulary annotation
[#1110] Set-based operations
[#1293] Update the Core vocabulary xml
[#1286] Context url for expand
[#1299] Enable write navigation property binding for containment target
[#1302] Update the DerivedTypeConstraint term in Validation vocabulary
[#1310] Add AllowedTerms/MinItems/MaxItems to Validation vocabulary
[#1308] Add LocalDateTime type definition into Core vocabulary
[#1312] Edm model validation for structural property type has wrong rule for property with type-definition type
[#1314] Update DerivedTypeConstraint appliesTo
[#1073] Better story for creating complex spatial types
[#1296] Remove Contentions from Microsoft.OData.Edm
[#1307] Add annotation term for Org.OData.Community.UrlEscape.V1.UrlEscapeFunction
This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.
-
ODataLib 7.5.3
Changes in ODataLib 7.5.3 Release
Notes
ODataLib 7.5.3 includes the following new features, bug fixes and improvements:
Features
[#1295] Enable derived type validation in Uri parsing
[#1341] Support read/write Edm.PrimitiveType, Edm.ComplexType, Edm.EntityType
[#1344] Enable expand transformation in $apply
[#1346] Introduce new ODataResourceValue class and support read/write ODataResourceValue
[#1358] Enable derived type validation in writing
Fixed Bugs
[#1323] Fix the escaped single-quote and Guid liternals for IN operator
[#1359] Fix SelectExpandNode for navigation properties on derived/complex types
Improvements
[#1349] Remove locks from Uri parser
[#1357] Use TryParse API instead of Parse for date to avoiding throwing exceptions
This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.
-
ODataLib 7.5.4
Changes in ODataLib 7.5.4 Release
Notes
ODataLib 7.5.4 includes the following new features, bug fixes and improvements:
Features
[#1376] Enable reading validation for derived type constraint annotation.
[#1285] Support customizing the built-in vocabulary models.
[#1404] Support reading/writing delta request payload.
Fixed Bugs
[#1368] Make aliases created in compute() transformation visible for following transforms/query options.
[#1373] IN operator not working with null value on nullable properties.
[#1385] & [#1164] Support parantheses and brackets in a CollectionConstantNode for IN operator.
[#1390] ODataUriParser can’t parse for function with all omitted optional parameters.
[#1391] Fix build uri problem with filter by Enum.
Improvements
[#1024] Improve the JSON reader buffer.
This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.
-
ODataLib 7.6.beta
Changes in ODataLib 7.6.beta Release
Notes
ODataLib 7.6.beta includes the following new features, bug fixes and improvements:
Features
[#1204] Support Large object stream (reader & writer).
[#1400] Enable Uri parser to parse escape function.
[#1414] Add the IEdmOperationReturn interface and enable annotation on return type of operation.
[#1422] & [#1428] Update capabilities, validation & authorization vocabularies.
[#1426] Properties defined in $compute and $apply could be used in a following query options ($select, $compute, $filter or $orderby).
Fixed Bugs
[#1260] Throw error when null value passed for collection of non-nullable complex type.
[#1409] Ensure that we could use aliases created in compute() in groupby.
[#1415] Build filter with “any” and “or” fails on keeping operations priority.
Improvements
[#1418] Use buffer when writing binary or byte array.
[#1420] Use array pool in JSON writer.
This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.
-
ODataLib 7.6.0
Changes in ODataLib 7.6.0 Release
Notes
ODataLib 7.6.0 includes the following new features, bug fixes and improvements on top of O:
Features
[#1440] Add the Example Term into Core vocabulary.
[#1454] Support versioned preferences for reading/writing OData prefix.
[#1459] Write nested entity reference link(s) in request/response.
[#1464] Enable to write the nextlink for the collection of entity reference links.
[#1476] Support reading/writing Edmx with Version=4.01.
Fixed Bugs
[#1318] Unescaped colons in relative Uri cause Invalid URI exception.
[#1451] FindType doesn’t work with alias-qualified names.
[#1455] Remove bogus validation error for EntitySetPath.
[#1463] Validate the resource type and resource set type in the same inheritance tree.
[#1465] Support out of line annotations can’t target enum member.
[#1467] Support enum parameters for Uri function.
[#1469] Add a validation rule about target of the annotation should be allowed in the AppliesTo of the term.
Improvements
[#1448] Refactor & Improve the ODataMediaTypeResolver.
[#1458] Align resource template and generated code.
[#1473] Reduce ToList calls in operation overload resolver.
[#1474] Fix tests disabled for large object streaming pull request.
This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.
5. ODATA FEATURES
-
Parsing uri path template
From ODataLib 6.11.0, OData uri parser can parse uri path template. A path template is any identifier string enclosed with curly braces. For example:
Uri templates
There are three kinds of template:
- Key template: ~/Customers({key})
- Function parameter template: ~/Customers/Default.MyFunction(name={name})
- Path template: ~/Customers/{dynamicProperty}
Be caution:
- In UriParser instance, please set EnableUriTemplateParsing = true.
- Path template can’t be the first segment.
Example
-
Add vocabulary annotations to EdmEnumMember
From ODataLib 6.11.0, it supports to add vocabulary annotations to EdmEnumMember.
Create Model
Output
<?xml version="1.0" encoding="utf-8"?> <edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"> <edmx:DataServices> <Schema Namespace="DefaultNamespace" xmlns="http://docs.oasis-open.org/odata/ns/edm"> <EnumType Name="Color" IsFlags="true"> <Member Name="Cyan" Value="1" /> <Member Name="Blue" Value="2" /> <Member Name="Red" Value="3"> <Annotation Term="DefaultNamespace.StringTerm" Qualifier="q1" String="Hello world!" /> </Member> </EnumType> <Term Name="StringTerm" Type="Edm.String" /> <EntityContainer Name="Container"> <EntitySet Name="Cars" EntityType="DefaultNamespace.Car" /> </EntityContainer> </Schema> </edmx:DataServices> </edmx:Edmx>
-
Add additional prefer header
odata.track-changes, odata.maxpagesize, odata.ContinueOnError are supported to add in prefer header since ODataLib 6.11.0.
Create request message with prefer header
Then in the http request header, we will have:
Prefer: odata.continue-on-error,odata.maxpagesize=1024,odata.track-changes
-
$skiptoken & $deltatoken
From ODataLib 6.12.0, it supports to parse $skiptoken & $deltatoken in query options.
$skiptoken
Let’s have an example:
We can do as follows to parse:
$deltatoken
Let’s have an example:
We can do as follows to parse:
-
Alternate Key
From ODataLib 6.13.0, it supports the alternate key. For detail information about alternate keys, please refer to here.
The related Web API sample codes can be found here.
Single alternate key
Edm Model builder
The following codes can be used to build the single alternate key:
Related Metadata
The following is the related metadata:
Multiple alternate keys
Edm Model builder
The following codes can be used to build the multiple alternate keys:
Related Metadata
The following is the related metadata:
Composed alternate keys
Edm Model builder
The following codes can be used to build the multiple alternate keys:
Related Metadata
The following is the related metadata:
Uri parser
Enable the alternate keys parser extension via the Uri resolver
AlternateKeysODataUriResolver
. -
Capabilities vocabulary support
From ODataLib 6.13.0, it supports the capabilities vocabulary. For detail information about capabiliites vocabulary, please refer to here.
Enable capabilities vocabulary
If you build the Edm model from the following codes:
The capabilities vocabulary is enabled as a reference model in the Edm Model.
How to use capabilities vocabulary
ODL doesn’t provide a set of API to add capabilites, but it provoides an unified API to add all vocabularies:
Let’s have an example to illustrate how to use capabilities vocabulary:
The related metata
The corresponding metadata can be as follows:
-
Write NextPageLink/Count for collection
From ODataLib 6.13.0, it supports to write the NextPageLink/Count instance annotation in top-level collection payload. Let’s have an example:
When you want to serialize a collection instance, you should first create an object of
ODataCollectionStart
, in which you can set the next page link and the count value.The payload looks like:
-
Override primitive serialization and deserialization of payload
Since ODataLib 6.12.0, it supports to customize the payload value converter to override the primitive serialization and deserialization of payload.
New public API
The new class
ODataPayloadValueConverter
provides a default implementation for value conversion, and also allows developer to override by implemementingConvertFromPayloadValue
andConvertToPayloadValue
.And in ODataLib 7.0, a custom converter is registered through DI.
Sample
Here we are trying to override the default converter to support the “R” format of date and time.
1. Define DataTimeOffset converter
2. Register new converter to DI container
Please refer here about DI details.
Then
DateTimeOffset
can be serialized toThu, 12 Apr 2012 18:43:10 GMT
, and payload likeThu, 12 Apr 2012 18:43:10 GMT
can be deserialized back toDateTimeOffset
. -
Allow serialization of additional properties
We now support serializing additional properties which are not advertised in metadata since ODataLib 6.13.0. To achieve this, users just need to turn off the
ThrowOnUndeclaredPropertyForNonOpenType
validation setting when constructingODataMessageWriterSettings
.Here is a full example which is trying to write an extra property
Prop1
in the entity. The implementation ofInMemoryMessage
used in this sample can be found here.Prop1
will appear in the payload: -
Expanded Navigation Property Support in Delta Response
From ODataLib 6.15.0, we introduced the support for reading and writing expanded navigation properties (either collection or single) in delta responses. This feature is not covered by the current OData spec yet but the official protocol support is already in progress. As far as the current design, expanded navigation properties can ONLY be written within any
$entity
part of a delta response. Every time an expanded navigation property is written, the full expanded resource set or resource should be written instead of just the delta changes because in this way it’s easier to manage the association among resources consistently. Inside the expanded resource set or resource, there are ONLY normal resource sets or resources. Multiple expanded navigation properties in a single$entity
part is supported. Containment is also supported.Basically the new APIs introduced are highly consistent with the existing ones for reading and writing normal delta responses so there should not be much trouble implementing this feature in OData services. This section shows how to use the new APIs.
Write expanded navigation property in delta response
There are only four new APIs introduced for writing.
The following sample shows how to write an expanded resource set (collection of resources) in a delta response. Please note that regardless of whether or not the nested resource info will be eventually written to the payload,
WriteStart(nestedResourceInfo)
MUST be called before actually callingWriteStart(expandedResourceSet)
to write an expanded resource set. So is for a single expanded resource.The next sample shows how to write a single expanded entity in a delta response.
Some internals behind the writer
Though there is only one
WriteStart(resource)
,ODataJsonLightDeltaWriter
keeps track of an internal state machine thus can correctly differentiate between writing a delta resource and writing a normal resource. Actually during writing the expanded navigation properties, all calls toWriteStart(resource)
,WriteStart(resourceSet)
andWriteStart(nestedResourceInfo)
are delegated to an internalODataJsonLightWriter
which is responsible for writing normal payloads. And the control will return toODataJsonLightDeltaWriter
after the internal writer completes. The internal writer pretends to write a phony resource but will skip writing any structural property or instance annotation until it begins to write a nested resource info (means we are going to write an expanded navigation property).Read expanded navigation property in delta response
In the reader part, new APIs include a new state enum and a sub state property. All the other remains the same.
Note that the sub state is
ODataReaderState
which is used for normal payloads. The sub state is a complement to the main state inODataDeltaReader
to specify the detailed reader state within expanded navigation properties. But the sub state is ONLY available and meaningful when the main state isODataDeltaReaderState.NestedResource
. Users can still use theItem
property to retrieve the current item being read out from an expanded payload.The following sample shows the scaffolding code to read the expanded resource set and resource in a delta payload.
Some internals behind the reader
Just as the implementation of the writer, there is also an internal
ODataJsonLightReader
to read the expanded payloads. When the delta reader reads a navigation property (can be inferred from the model) in the$entity
part of a delta response, it creates the internal reader for reading either top-level feed or top-level entity. For reading delta payload, there are some hacks insideODataJsonLightReader
to skip parsing the context URLs thus eachODataResource
being read out has no metadata builder with it. Actually the expanded payloads are treated in the same way as the nested payloads when reading parameters. This is currently a limitation which means the service CANNOT get the metadata links from a normal entity in a delta response. The internal reader also skips the next links after an expanded resource set because the$entity
part of a delta payload is non-pageable. When the internal reader is consuming the expanded payload, the delta reader remains at theNestedResource
state until it detects the state of the internal reader to beCompleted
. However we still leave theStart
andCompleted
states catchable to users so that they can do something before and after reading an expanded navigation property. -
Basic Uri parser support for aggregations
From ODataLib 6.15.0, we introduced the
basic
Uri parser support for aggregations, this is first step for us to support aggregations, Issues and PR to make this support better is very welcome, details about aggregation in spec can be found here.Aggregate
The aggregate transformation takes a comma-separated list of one or more aggregate expressions as parameters and returns a result set with a single instance, representing the aggregated value for all instances in the input set.
Examples
GET ~/Sales?$apply=aggregate(Amount with sum as Total)
GET ~/Sales?$apply=aggregate(Amount with min as MinAmount)
Open Issue
Groupby
The groupby transformation takes one or two parameters and
Splits
the initial set into subsets where all instances in a subset have the same values for the grouping properties specified in the first parameter,Applies
set transformations to each subset according to the second parameter, resulting in a new set of potentially different structure and cardinality,Ensures
that the instances in the result set contain all grouping properties with the correct values for the group,Concatenates
the intermediate result sets into one result set.Examples
GET ~/Sales?$apply=groupby((Category/CategoryName))
GET ~/Sales?$apply=groupby((ProductName), aggregate(SupplierID with sum as SupplierID))
Apply with other QueryOptions
Apply queryoption will get parse first and we can add filter, orderby, top, skip with apply.
Examples
$apply=groupby((Address/City))&$filter=Address/City eq 'redmond'
$apply=groupby((Name))&$top=1
$apply=groupby((Address/City))&$orderby=Address/City
Test
All the support scenarios can be found in WebAPI case, ODL case.
-
Customizable type facets promotion in URI parsing
Class
ODataUri
has two propertiesFilter
andOrderBy
that are tree data structures representing part of the URI parsing result. Precision and scale are two type facets for certain primitive types. When an operation is applied to types with different facets, they will first be converted to a common type with identical facets. The common type is also the type for the result of the operation. This conversion will appear as one or more convert nodes in the resulting tree. The question is, what are the type facets conversion/promotion rules?In the library, the rules are customizable. The interface is formulated by the following class:
The first method defines the rule to resolve two precision values
left
andright
to a common one, while the second is for scale values. The class provides a default implementation for the methods, which works as the default conversion rules. To customize the rules, you subclass and override the relevant methods as necessary.The default conversion rule (for both precision and scale) is as follows:
- if both
left
andright
arenull
, returnnull
; - if only one is
null
, return the other; - otherwise, return the larger.
You plugin a specific set of conversion rules by setting the
ODataUriResolver.TypeFacetsPromotionRules
property that has a declared type ofTypeFacetsPromotionRules
. If not explicitly specified by the user, an instance of the base classTypeFacetsPromotionRules
will be used by default.Let’s see a simple example. Consider the expression
Decimal_6_3 mul Decimal_5_4
whereDecimal_6_3
andDecimal_5_4
are both structural properties ofEdm.Decimal
type. The former has precision 6 and scale 3, while the latter has 5 and 4. Using the default conversion rules, the result would be: - if both
-
Navigation property under complex type
Since OData V7.0, it supports to add navigation property under complex type. Basically navigation under complex are same with navigation under entity for usage, the only differences are: 1. Navigation under complex can have multiple bindings with different path. 2. Complex type does not have id, so the navigation link and association link of navigation under complex need contain the entity id which the complex belongs to.
The page will include the usage of navigation under complex in EDM, Uri parser, and serializer and deserializer.
1. EDM
Define navigation property to complex type
Please note that only unidirectional navigation is supported, since navigation property must be entity type, so bidirectional navigation property does not make sense to navigation under complex type.
Add navigation property binding for navigation property under complex
When entity type has complex as property, its corresponding entity set can bind the navigation property under complex to a specified entity set. Since multiple properties can be with same complex type, the navigation property under complex can have multiple bindings with different path.
A valid binding path for navigation under complex is:
[ qualifiedEntityTypeName "/" ] *( ( complexProperty / complexColProperty ) "/" [ qualifiedComplexTypeName "/" ] ) navigationProperty
For example:
The navigation property
navUnderComplex
is binded to cities1 and cities2 with path"Address/City"
and"Addresses/City"
respectively.Then the csdl of the model would be like:
<Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="Sample"> <EntityType Name="City"> <Key> <PropertyRef Name="Name"/> </Key> <Property Name="Name" Nullable="false" Type="Edm.String"/> </EntityType> <ComplexType Name="Address"> <Property Name="Road" Type="Edm.String" Nullable="false" /> <NavigationProperty Name="City" Nullable="false" Type="Sample.City"/> </ComplexType> <EntityType Name="Person"> <Key> <PropertyRef Name="UserName"/> </Key> <Property Name="UserName" Nullable="false" Type="Edm.String"/> <Property Name="Address" Nullable="false" Type="Sample.Address"/> <Property Name="Addresses" Nullable="false" Type="Collection(Sample.Address)"/> </EntityType> <EntityContainer Name="Container"> <EntitySet Name="People" EntityType="Sample.Person"> <NavigationPropertyBinding Target="Cities1" Path="Address/City"/> <NavigationPropertyBinding Target="Cities2" Path="Addresses/City"/> </EntitySet> <EntitySet Name="Cities1" EntityType="Sample.City"/> <EntitySet Name="Cities2" EntityType="Sample.City"/> </EntityContainer> </Schema>
The binding path may need include type cast. For example, if there is a navigation property
City2
defined in a complex typeUsAddress
which is derived fromAddress
. If add a binding toCity2
, it should be like this:people.AddNavigationTarget(navUnderDerivedComplex, cities1, new EdmPathExpression("Address/Sample.UsAddress/City2"));
Here we do not include type cast sample to keep the scenario simple.Find navigation target for navigation property under complex
Since a navigation property can be binded to different paths, the exact binding path must be specified for finding the navigation target. For example:
Cities1
will be returned for this case.2. Uri
Query
Here lists some sample valid query Uris to access navigation property under complex:
Path
http://host/People('abc')/Address/City
Accessing navigation property under collection of complex is not valid, since item in complex collection does not have a canonical Url. That is to say,
http://host/People('abc')/Addresses/City
is not valid,City
underAddresses
can only be accessed through$expand
.Query option
Different with path, navigation under collection of complex can be accessed directly in expressions of $select and $expand, which means
Addresses/City
is supported. Refer ABNF for more details.$select: http://host/People('abc')/Address?$select=City http://host/People?$select=Address/City http://host/People?$select=Addresses/City $expand: http://host/People('abc')/Address?$expand=City http://host/People?$expand=Address/City http://host/People?$expand=Addresses/City $filter: http://host/People?$filter=Address/City/Name eq 'Shanghai' http://host/People('abc')/Addresses?$filter=City/Name eq 'Shanghai' http://host/People?$filter=Addresses/any(a:a/City/Name eq 'Shanghai') $orderby: http://host/People?$order=Address/City/Name
Uri parser
There is nothing special if using
ODataUriParser
. ForODataQueryOptionParser
, if we need resolve the navigation property under complex to its navigation target in the query option, navigation source that the complex belongs to and the binding path are both needed. If the navigation source or part of binding path is in the path, we need it passed to the constructor ofODataQueryOptionParser
. So there are 2 overloaded constructor added to acceptODataPath
as parameter.Note: Parameter IServiceProvider is related to Dependency Injection.
Actually we do not recommend to use
ODataQueryOptionParser
in this case,ODataUriParser
would be more convenient. Here we still give an example just in case:3. Serializer (Writer)
Basically, the writing process is same with writing navigation under entity. Let’s say we are writing an response of query
http://host/People('abc')?$expand=Address/City
.Sample code:
Payload:
{ "@odata.context": "http://host/$metadata#People/$entity", "UserName":"abc", "Address": { "Road":"def", "City": { "Name":"Shanghai" } } }
4. Deserializer (Reader)
Reading process is same with reading an navigation property under entity. For navigation property
City
underAddress
, it will be read as anODataNestedResourceInfo
which has navigation urlhttp://host/People('abc')/Complex/City
and anODataResource
which has Idhttp://host/Cities1('Shanghai')
. -
Merge complex type and entity type
From ODataLib 7.0, we merged the public APIs for serializing/deserializing complex values and entities. We did this because complex type and entity type are becoming more and more similar in the protocol, but we continue to pay overhead to make things work for both of them. Since the only really fundamental differences between complex type and entity type at this point are the presence of a key and media link entries, we thought it was best to merge them and just deal with the differences.
We followed the existing implementation of serializing/deserializing entity instances for both entity instances and complex instances, and the implementation of serializing/deserializing entity collections for both entity collections and complex collections. This page will provide some simple sample code about how to write these kinds of payload.
Public APIs used to store complex/entity instances
ODataResource
class is used to store information of an entity instance or a complex instance.ODataResourceSet
class is used for both a collection of entity or a collection of complex.ODataNestedResourceInfo
class is used for both navigation property and complex property. For complex property, this class will be used to store the name of the complex property and a Boolean to indicate whether this property is a single instance or a collection.For other Public APIs, you can refer to the Breaking changes about Merge Entity and Complex.
Model
Suppose we have a model, in following sections, we will explain how to write/read a complex property or a collection complex property.
<?xml version="1.0" encoding="utf-8"?> <edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"> <edmx:DataServices> <Schema Namespace="SampleService" xmlns="http://docs.oasis-open.org/odata/ns/edm"> <ComplexType Name="Location" OpenType="true"> <Property Name="Address" Type="Edm.String" Nullable="false" /> <Property Name="City" Type="Edm.String" Nullable="false" /> </ComplexType> <EntityType Name="Person" OpenType="true"> <Key> <PropertyRef Name="UserName" /> </Key> <Property Name="UserName" Type="Edm.String" Nullable="false" /> <Property Name="HomeAddress" Type="SampleService.Location" /> <Property Name="OtherAddresses" Type="Collection(SampleService.Location)" /> </Property> </EntityType> <EntityContainer Name="DefaultContainer"> <EntitySet Name="People" EntityType="SampleService.Person" /> </EntityContainer> </Schema> </edmx:DataServices> </edmx:Edmx>
Write a top level complex instance
In order to write the complex property payload, we need to create an
ODataWriter
first. ODataLib providesODataMessageWriter.CreateODataResourceWriter
to create the writer.Then, we can write a complex property just like writing an entity by using
WriteStart
andWriteEnd
.If we want to write a complex collection property, we can use
CreateODataResourceSetWriter
and write the complex collection property.Write a complex property in an entity instance
To write an entity with a complex property, we can create the
ODataWriter
for the entity by callingCreateODataResourceWriter
, and then write the entity. we write the complex property just as writing a navigation property.- Write the property name of the complex property by
WriteStart(ODataNestedResourceInfo nestedResourceInfo)
- Write the complex instance by
WriteStart(ODataResource resource)
. WriteEnd()
for each part.
Sample:
To write a complex collection property in an entity, we need to set
ODataNestedResourceInfo.IsCollection
is set to true, and then write the resource set.Developers can follow the same way to write a nested complex property in a complex instance.
Write complex or entity instances in Uri parameters
To write an entity (or a complex) instance or a collection of entity (or a collection of complex) in Uri parameters, ODataLib provides
ODataMessageWriter.CreateODataUriParameterResourceWriter
andODataMessageWriter.CreateODataUriParameterResourceSetWriter
to create theODataWriter
. Then, you can follow the same sample code in the above two sections to write the related parameters.Read a top level complex instance
To read a complex (or an entity) instance, ODataLib provides
ODataMessageReader.CreateODataResourceReader
to create theODataReader
.Then, developers can read the complex (or entity) instance, nested complex ( or complex collection) properties or navigation properties.
if this reader is created for an entity type, the same code can be used to read an entity instance.
Read a top level complex collection
To read a complex collection ( or an entity collection) , ODataLib provides
ODataMessageReader.CreateODataResourceSetReader
to create theODataReader
.if this reader is created for an entity collection, the same code can be used to read an entity collection.
Read complex or entity instances in Uri parameters
To read an entity (or a complex) instance or a collection of entity (or a collection of complex) in Uri parameters, ODataLib provides
ODataMessageReader.CreateODataUriParameterResourceReader
andODataMessageReader.CreateODataUriParameterResourceSetReader
to create theODataReader
. Then, you can follow the same sample code in the above two sections to read the related parameters. - Write the property name of the complex property by
-
OData Uri Path Parser Extensibility
In order to support more comprehensive OData Uri path, from ODataLib 7.0, we support Uri path parser customization in two parts:
- Allow developers to customize how to separate a Uri into segments in string.
- Allow developers to customize how to bind those raw string segments unrecognized by
ODataUriParser
to the model and createODataPathSegments
.
For example, we have a Uri http://localhost/odata/drives(‘C’)/root:/OData/Docs/Features/Uri%20Parser%20Path%20Extensibility.doc:/size which is used to get the size of a local file “C:\OData\Docs\Features\Uri Parser Path Extensible.doc”. But
ODataUriParser.ParsePath()
doesn’t know how to bind this path to the edm model. So we want to provide a way to let developers define how to parse it.Following sections will provide some sample codes to support this feature.
Model
Given a model as following:
<?xml version="1.0" encoding="utf-8"?> <edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"> <edmx:DataServices> <Schema Namespace="SampleService" xmlns="http://docs.oasis-open.org/odata/ns/edm"> <EntityType Name="drive"> <Key> <PropertyRef Name="id" /> </Key> <Property Name="id" Type="Edm.String" Nullable="false" /> <NavigationProperty Name="items" Type="Collection(SampleService.item)" ContainsTarget="true" /> </EntityType> <EntityType Name="item" OpenType="true"> <Key> <PropertyRef Name="id" /> </Key> <Property Name="id" Type="Edm.String" Nullable="false" /> <Property Name="size" Type="Edm.Int64" /> </EntityType> <EntityContainer Name="SampleService"> <EntitySet Name="drives" EntityType="SampleService.drive" /> </EntityContainer> </Schema> </edmx:DataServices> </edmx:Edmx>
Separate a Uri into Segments
UriPathParser
provides a public virtual APIParsePathIntoSegments(Uri fullUri, Uri serviceBaseUri)
to customize how to separate a Uri into segments in raw string.Developers can define their own
UriPathParser
, register this class by DI (Please refer to Dependency Injection support) and override theParsePathIntoSegments
. Then, ODataLib will use this API to separate the Uri into several segments.public class UriPathParser { public virtual ICollection<string> ParsePathIntoSegments(Uri fullUri, Uri serviceBaseUri) }
Customized
UriPathParser
public class CustomizedUriPathParser : UriPathParser { public UriPathParserType UriPathParserType { get; set; } public CustomizedUriPathParser(ODataUriParserSettings settings) : base(settings) { } public override ICollection<string> ParsePathIntoSegments(Uri fullUri, Uri serviceBaseUri) { Uri uri = fullUri; int numberOfSegmentsToSkip = 0; numberOfSegmentsToSkip = serviceBaseUri.AbsolutePath.Split('/').Length - 1; string[] uriSegments = uri.AbsolutePath.Split('/'); List<string> segments = new List<string>(); List<string> tmpSegments = new List<string>(); bool end = true; for (int i = numberOfSegmentsToSkip; i < uriSegments.Length; i++) { string segment = uriSegments[i]; if (!segment.StartsWith("root:") && segment.EndsWith(":")) { tmpSegments.Add(segment); segments.Add(Uri.UnescapeDataString(string.Join("/", tmpSegments))); end = true; } else if (segment.StartsWith("root:") || !end) { end = false; tmpSegments.Add(segment); continue; } else { segments.Add(segment); } } return segments.ToArray(); } }
This class defines its own
ParsePathIntoSegments
to separate the Uri into segments. Developers can register this class bybuilder.AddService<UriPathParser, CustomizedUriPathParser>(ServiceLifetime.Scoped)
.ParsePathIntoSegments
considers “root:/OData/Docs/Features/Uri%20Parser%20Path%20Extensibility.doc:” as one segment. It parsed the whole Uri into three segments in string.[0]: "drives('C')" [1]: "root:/OData/Docs/Features/Uri Parser Path Extensibility.doc:" [2]: "size"
Bind Unknown Raw-string Segments to Model
After the above step, developers now can define how to bind the “root:/OData/Docs/Features/Uri Parser Path Extensibility.doc:” which is unknown for
ODataUriParser
.For all unknown segments, ODataLib provides a
DynamicPathSegment
class to represent the meaning of them in model.DynamicPathSegment
could be used for both open property segments or other dynamic path segments.ODataUriParser
also provides a PropertyParseDynamicPathSegmentFunc
for developers to bind those unknown raw-string segments to model.public delegate ICollection<ODataPathSegment> ParseDynamicPathSegment(ODataPathSegment previous, string identifier, string parenthesisExpression); public ParseDynamicPathSegment ParseDynamicPathSegmentFunc { get { return this.configuration.ParseDynamicPathSegmentFunc; } set { this.configuration.ParseDynamicPathSegmentFunc = value; } }
By default, if developers do not set the
ParseDynamicPathSegmentFunc
, ODataLib will consider the unknown segment as an open property. Or, ODataLib will use this function to bind the segment and return a collection ofODataPathSegment
. Then, ODataLib will parse the following segment according to this binding result.-
Developers can set
ParseDynamicPathSegmentFunc
as following which parses the second segment into aDynamicPathSegment
withIEdmEntityType
“Item”. ThenODataUriParser
will parse the last segment “size” into aPropertySegment
.uriParser.ParseDynamicPathSegmentFunc = (previous, identifier, parenthesisExpression) => { switch (identifier) { case "root:/OData/Docs/Features/Uri Parser Path Extensibility.doc:": return new List<ODataPathSegment> { new DynamicPathSegment(identifier, itemType, true) }; default: throw new Exception("Not supported Type"); } };
Then, the
ODataPath
should be:[0]: EntitySetSegment : "drives" [1]: KeySegment : "C" [2]: DynamicPathSegment: "root:/OData/Docs/Features/Uri Parser Path Extensibility.doc:" [3]: PropertySegment: "size"
-
Developers can also set
ParseDynamicPathSegmentFunc
as following which will translate the unknown string segment into aNavigationPropertySegment
andKeySegment
:uriParser.ParseDynamicPathSegmentFunc = (previous, identifier, parenthesisExpression) => { switch (identifier) { case "root:/OData/Docs/Features/Uri Parser Path Extensibility.doc": return new List<ODataPathSegment> { new NavigationPropertySegment(navProp, navSource);, new KeySegment(new Dictionary<string, object>() { { "id", "01VL3Q7L36JOJUAPXGDNAZ4FVIGCTMLL46" } }, itemType, navSource); }; default: throw new Exception("Not supported Type"); } };
Then, the
ODataPath
should be:[0]: EntitySetSegment : "drives" [1]: KeySegment : "C" [2]: NavigationPropertySegment: "items" [3]: KeySegment: "01VL3Q7L36JOJUAPXGDNAZ4FVIGCTMLL46" [4]: PropertySegment: "size"
On server side, developers can implement their own behavior of CRUD according to the parse result.
-
Navigation property partner
The library supports setting and retrieving partner information of navigation properties.
The following APIs can be used to set partner information:
The former is general-purpose while the latter is a convenient shortcut for certain scenarios. Let’s look at the first method. To use this method, you must first have a top-level navigation property
navigationProperty
already defined on the entity type, and another possibly nested navigation propertypartnerNavigationProperty
already defined on the target entity type. This method then sets the partner information of the two navigation properties. SincenavigationProperty
is a top-level navigation property defined on the entity type on which the method is invoked,navigationPropertyPath
is simply the name of the navigation property.partnerNavigationProperty
, on the other hand, could be defined on either an entity or complex type.partnerNavigationPropertyPath
specifies the path to the partner navigation property on the target entity type. For example, if the partner navigation property is calledInnerNav
which is defined on a complex-typed propertyComplexProp
which is a top-level structural property defined on the target entity type, thenpartnerNavigationPropertyPath
should be passed something likenew EdmPathExpression("ComplexProp/InnerNav")
. According to the spec, the partner information must not be set for navigation properties defined on a complex type. So, in this case, partner information will only be set onnavigationProperty
, but notpartnerNavigationProperty
. Let’s give another example, say the partner navigation property is calledNav
which is a top-level navigation property defined on the target entity type. ThenpartnerNavigationPropertyPath
should be passed something likenew EdmPathExpression("Nav")
. In this case, the partner information will be set for bothnavigationProperty
andpartnerNavigationProperty
.The second method is a shortcut for the case when both navigation properties are top-level properties defined on the entity types. It doesn’t work for the case when one navigation property is defined on a complex type, and thus a nested property on the entity type. It doesn’t require you to define the navigation properties first; it will do this on your behalf. In effect, it will first add the two navigation properties on the entity types, and then set their partner information.
The following APIs are provided to retrieve the partner information of a navigation property:
The first is used to retrieve the partner navigation property itself, while the second is used to retrieve the path to the partner navigation property within the target entity type.
-
Override type annotation in serialization
As you may know, all the OData items (
ODataResource
,ODataResourceSet
, etc.) read from the payload are inherited fromODataAnnotatable
. InODataAnnotatable
, there is a property calledTypeAnnotation
whose type isODataTypeAnnotation
. It is used to store the@odata.type
annotation read from or written to the payload. The reasons why we don’t merge it into the instance annotations inODataAnnotatable
are that: 1) for performance improvement; 2) in ATOM format, we also have OData type annotation.During deserialization, the
TypeAnnotation
property will be set by the OData readers into each OData item read from the payload. During serialization, theTypeAnnotation
property itself and itsTypeName
property together will control how OData type annotation will be written to the payload.The control policies are:
- If the
TypeAnnotation
property itself isnull
, then we will rely on the underlying logic in ODataLib and Web API OData to determine what OData type annotation to be written to the payload. - If the
TypeAnnotation
property is notnull
but theTypeName
property isnull
, then absolutely NO OData type annotation will be written to the payload. - If the
TypeAnnotation
property is notnull and the
TypeNameproperty is not
null` as well, then the string value specified will be written to the payload.
That said, if you specify a value for the
TypeAnnotation
property, itsTypeName
property will override the underlying logic anyway. So please pay attention to the value of theTypeName
property. - If the
-
Support multi-NavigationPropertyBindings for a single navigation property
According to the spec, a navigation property can be used in multiple bindings with different path. It makes a lot of sense for navigation property under complex and containment. From ODataLib V7.0, we are able to support it. The valid format of a binding path is:
For example, we have containment in Trippin service:
Person
|—- Trips (containment)
|—- PlanItems (containment)
|—- NS.Flight (derived type of PlanItem)
|—- Airline (navigation property)Now we bind entityset
Airlines
to the propertyAirline
. Since for navigation property binding, we need start from non-containment entityset, then route to navigation property with binding path. So we should have binding:If we have
InternationalTrips
underPerson
which has typeTrip
as well, we can have binding<NavigationPropertyBinding Path="InternationalTrips/PlanItems/NS.Flight/Airline" Target="Airlines"/>
underPeople
as well. Please note that current Trippin implementation for this part does not have strict compliance with spec due to prohibition of breaking changes.Let’s have another example of complex which have multi bindings.
City
is a navigation property of complex typeAddress
, andPerson
hasHomeAddress
,CompanyAddress
which are bothAddress
type. Then we can have binding:For single binding, binding path is just the navigation property name or type cast appending navigation property name. But for multiple bindings, binding path becames an essential info to create a binding. As a result, following APIs are added:
EDM
public EdmNavigationPropertyBinding (IEdmNavigationProperty navigationProperty, IEdmNavigationSource target, IEdmPathExpression bindingPath)
Use this API to create EdmNavigationPropertyBinding instance if the bindingpath is not navigation property name.public void AddNavigationTarget (IEdmNavigationProperty navigationProperty, IEdmNavigationSource target, IEdmPathExpression bindingPath)
Add a navigation property binding and specify the whole binding path.public virtual Microsoft.OData.Edm.IEdmNavigationSource FindNavigationTarget (IEdmNavigationProperty navigationProperty, IEdmPathExpression bindingPath)
Find navigation property with its binding path.ODL
public ODataQueryOptionParser (IEdmModel model, ODataPath odataPath, String queryOptions)
public ODataQueryOptionParser(IEdmModel model, ODataPath odataPath, IDictionary<string, string> queryOptions, IServiceProvider container)
Possibly need ODataPath to resolve navigation target of segments in query option if the navigation property binding path is included in both path and query option. Refer: Navigation property under complex type.Take the above complex scenario for example. For generating this kind of model, we need use the new AddNavigationTarget API, and different navigation target can be specified:
cityOfAddress
is the variable to present navigation propertyCity
underAddress
, andcities1
andcities2
are different entityset based on entity typeCity
.To achieve the navigation target, the new FindNavigationTarget API can be used:
-
Centralized control for OData Simplified Options
In previous OData library, the control of writing/reading/parsing key-as-segment uri path is separated in ODataMessageWriterSettings/ODataMessageReaderSettings/ODataUriParser. The same as writing/reading OData annotation without prefix. From ODataV7.0, we add a centialized control class for them, which is ODataSimplifiedOptions.
The following API can be used in ODataSimplifiedOptions:
ODataSimplifiedOptions is registered by DI with singleton ServiceLifetime (Please refer to Dependency Injection support).
-
Read and Write Untyped values in ODataLib
Starting with ODataLib 7.0,
ODataMessageReader
&ODataMessageWriter
are able to read & write untyped primitive, structured, and collection values.Values read from a payload are considered untyped if:
- They represent a property not defined in $metadata and do not contain a property annotation
- The specified type cannot be resolved against $metadata, or
- They are explicitly declared as Edm.Untyped, or Collection(Edm.Untyped)
The
ODataMessageReaderSettings
&ODataMessageWriterSettings
Validations
property can be set with~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType
to enable reading/writing undeclared & untyped value properties in a payload.Untyped values can be read and written as raw strings or, starting with ODataLib 7.3, can be read and written as structured values.
For compatiblity with ODataLib 7.0, untyped values are read by default as a raw string representing the untyped content. To use the standard OData reader APIs to read untyped content in ODataLib 7.3, set the
ODataMessageReaderSettings.ReadUntypedAsString
property tofalse
.Given the following model:
and the following payload, in which the second property (UndeclaredAddress1) has a type (Server.NS.UndefinedAddress) that is not defined in metadata:
the following code will read the content of the second property as a raw string.
By setting
ReadUntypedAsString
tofalse
, the same content can be read as a structure value:Note that a new reader state,
ODataReaderState.Primitive
, is added in ODataLib 7.4 in order to support reading primitive values within an untyped collection. Null values within an untyped collection continue to be read as null resources.By default, untyped primitive values are returned as boolean if the JSON value is a boolean value, as decimal if the JSON value is numeric, otherwise as string. A custom primitive type resolver can be used in order to return a more specific type based on the value. If the custom type resolver returns null, then the default resolution is applied.
For example, the following custom primitive type resolver will recognize strings that look like date, time of day, datetimeoffset, duration, and guid values, will differentiate between integer and decimal values, and will resolve “Male” or “Female” as an enum value from the
Server.NS.Gender
enum type.To write a raw string into the payload, create an
ODataUntypedValue
and set theRawValue
to the value to be written. Note that there is no validation of the contents of the string written to the payload and it will break clients if it is malformed or if it does not match the expected content-type of the payload.As of ODataLib 7.3, it is possible to write primitive, structured, and collection values to an untyped property, or within an untyped collection. A new
WritePrimitive()
method is added to theODataWriter
for writing anODataPrimitiveValue
within an untyped collection.For example, the following writes an untyped collection containing a null, followed by a structured value with a single property, followed by a nested collection containing two primitive values:
-
Optional Parameters
Starting with ODataLib 7.3, parameters can be marked as optional. Optional parameters may be omitted when invoking a custom function.
Function resolution will first attempt to find an overload that exactly matches the specified function. If there is no exact match, it will select the function overload that matches all specified parameters and for which all required parameters are specified, and fail if there is more than one such function.
Optional parameters are annotated in CSDL using the new
Org.OData.Core.V1.OptionalParameter
annotation term:"<Parameter Name = "optionalParam" Type="Edm.String">" "<Annotation Term="Org.OData.Core.V1.OptionalParameter"/>" "</Parameter>"
An optional parameter may optionally specify a default value. The default value is purely informational, and conveys to the consumer the value that will be used if the parameter is not supplied:
"<Parameter Name = "optionalParamWithDefault" Type="Edm.String">" "<Annotation Term="Org.OData.Core.V1.OptionalParameter">" "<Record>" "<PropertyValue Property="DefaultValue" String="Smith"/>" "</Record>" "</Annotation>" "</Parameter>"
Any optional parameters must be come after all non-optional parameters for a function or action.
When reading CSDL, optional values are returned as parameters that support
IEdmOptionalParameter
:When building a model, optional parameters may be specified using the new
EdmOptionalParameter
class: -
Json batch format in ODataLib
Introduction to JSON Batching
Overview
Similar to Windows batch command, OData supports grouping multiple requests together, sending a single HTTP request to OData service, and grouping corresponding responses together before sending back as a single HTTP response. The major benefits for request batching are to reduce client/server round trips and to support atomic operation.
The batch format supported by OData core libraries is multipart/mime for OData protocol up to v4.0. To accommodate the need for a more developer-friendly format, the new JSON format batching support is added to the latest version of OData protocol v4.01. The JSON format batching format also brings another major benefit of allowing requests inside a batch to be executed in specified orders.
The OData v4.01 protocol specification can be found on OASIS site. The most current version is: http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#_Toc499805328.
Details of the JSON Batch format can be found in the OData JSON Format v4.01 specification on the OASIS site.. The most current version is: http://docs.oasis-open.org/odata/odata-json-format/v4.01/odata-json-format-v4.01.html#_Toc499716905
Sample Batch Request in JSON Format
Here is one sample batch request in JSON format (unnecessary details are omitted for sake of brevity):
https://localhost:9000/$batch User-Agent: Fiddler Authorization: <authz token> Content-Type: application/json Accept: application/json Host: localhost:9000 Content-Length: 1234 { "requests": [ { "method": "POST", "atomicityGroup": "g1", "url": "https://localhost:9000/users", "headers": { "content-type": "application/json; odata.metadata=minimal; odata.streaming=true", "odata-version": "4.0" }, "id": "g1-r1", "body": {"upn": "u1@domain.com", "givenName": "Jon", "surname": "Doe"} }, { "id": "r2", "method": "PATCH", "url": "https://localhost:9000/users('u2@domain.com')", "headers": { "content-type": "application/json; odata.metadata=minimal; odata.streaming=true", "odata-version": "4.0" }, "body": {"givenName":"Jonx"} }, { "id": "g2-r5", "atomicityGroup": "g2", "method": "POST", "url": "https://localhost:9000/users", "headers": { "content-yype": "application/json; odata.metadata=minimal; odata.streaming=true", "odata-version": "4.0" }, "body": {"upn": "u5@domain.com", "givenName": "Jon", "surname": "Doe"} }, { "id": "g2-r6", "atomicityGroup": "g2", "method": "POST", "url": "https://localhost:9000/users", "headers": { "content-type": "application/json; odata.metadata=minimal; odata.streaming=true", "odata-version": "4.0" }, "body": {"upn": "u6@domain.com", "givenName": "Jon", "surname": "Doe"} }, { "id": "r8", "method": "get", "url": "https://localhost:9000/users", "headers": { "content-type": "application/json; odata.metadata=minimal; odata.streaming=true", "odata-version": "4.0" }, "dependsOn": ["g1", "g2", "r2"] } ] }
Significant attributes for the JSON batch request are as follow:
- payload is in JSON format, an expansion to the original multipart/mixed format.
- A JSON batch request has only one top level property named
requests
, whose value is an array containing multiple requests and multiple atomic groups, each of which can contain multiple requests. - Request headers
Content-Type
andAccept
designates the batch request and response format to be JSON batching. For comparison, inmultipart/mime
format these two headers areMultipart/Mixed
with optional content type parameters. These are the header values driving the batch processing while achieving ODL compatibility of two batch formats. Typical use case is that request & response are of the same format, but it should not be a limitation for the ODL Core library batching design, as long as the header values are consistent with the payload content in the request & response. Content-Length
header of the batch request specifies the content length of the JSON batch request payload.- Requests belonged to same atomic group are denoted by the
atomicityGroup
attribute having the same value, e.g. “g2”. JSON batch format requires that all requests belonged to same group must be adjacent in therequests
array. - The
dependsOn
attribute can be used to specify an array of request prerequisites of this request. Here are the important rules fordependsOn
derived from the spec for JSON Batch semantics:- If request URL references an entity from another request, id of the corresponding request must also be included;
- Forward referencing is not allowed;
- Referencing id of request that is part of a group is not allowed, with the exception that the referenced requests are in the same group of this request.
In the example above, with JSON batch format, this request specifies the following:
- Contains two atomic groups
g1
andg2
; - Request
g1-r1
needs to be executed beforeg2-r6
, which needs to be beforer8
; - Request
r2
can be executed in arbitrary order; - Request
g2-r5
can be executed either before or afterg1
, but needs to be executed beforer8
. Additionally, sinceg2-r5
andg2-6r
belong to the same atomic groupg2
, execution results need to be roll-backed if either of them failed (service specific implementation).
Typical JSON Batch Request Creation using OData-Core library
With user-defined EDM model, user can use ODL-Core library to create JSON batch request directly, similar to Multipart/Mixed batch creation. For example:
// The following code snippet generates JSON batch payload into a memory stream. MemoryStream stream = new MemoryStream(); IODataRequestMessage requestMessage = CreateRequestMessage(stream); requestMessage.SetHeader("Content-Type", "application/json"); ODataMessageWriterSettings settings = new ODataMessageWriterSettings(); using (ODataMessageWriter messageWriter = new ODataMessageWriter(requestMessage, settings)) { ODataBatchWriter batchWriter = messageWriter.CreateODataBatchWriter(); batchWriter.WriteStartBatch(); string createRequestId = "contentId-1"; batchWriter.WriteStartChangeset(); ODataBatchOperationRequestMessage createOperationMessage = batchWriter.CreateOperationRequestMessage( "POST", new Uri(string.Format(CultureInfo.InvariantCulture, "{0}/{1}", serviceDocumentUri, "MySingleton")), createRequestId); using (ODataMessageWriter operationMessageWriter = new ODataMessageWriter(createOperationMessage)) { ODataWriter entryWriter = operationMessageWriter.CreateODataResourceWriter(); ODataResource entry = new ODataResource(); // skipped customized data population for initialization here... entryWriter.WriteStart(entry); entryWriter.WriteEnd(); } // A PATCH operation that depends on the preceding POST operation. List<string> dependsOnIds = new List<string> { createRequestId }; ODataBatchOperationRequestMessage updateOperationMessage = batchWriter.CreateOperationRequestMessage( "PATCH", new Uri(string.Format(CultureInfo.InvariantCulture, "{0}/{1}", serviceDocumentUri, "$" + createRequestId)), "updateReuqestId-1", BatchPayloadUriOption.AbsoluteUri, dependsOnIds); using (ODataMessageWriter operationMessageWriter = new ODataMessageWriter(updateOperationMessage)) { var entryWriter = operationMessageWriter.CreateODataResourceWriter(); var entry = new ODataResource(); // skipped customized data population for update here... entryWriter.WriteStart(entry); entryWriter.WriteEnd(); } batchWriter.WriteEndChangeset(); ODataBatchOperationRequestMessage queryOperationMessage = batchWriter.CreateOperationRequestMessage( "GET", new Uri(string.Format(CultureInfo.InvariantCulture, "{0}/{1}", serviceDocumentUri, "MySingleton")), "readOperationId-1"); queryOperationMessage.SetHeader("Accept", "application/json;odata.metadata=full"); batchWriter.WriteEndBatch(); } // stream contains the JSON batch request payload.
Note that in JSON batch,
dependsOnIds
needs to include the request Id being referenced in the request’s URL, as required by the JSON batch semantics.Typical JSON Batch Request/Response Processing by an OData Service
An
ODataBatchReader
can be instantiated to process the batch request as follows:ODataMessageReader odataMessageReader = new ODataMessageReader(odataRequestMessage, messageReaderSettings, model); ODataBatchReader odataBatchReader = odataMessageReader.CreateODataBatchReader();
Note that batch format is detected during the instantiation of
ODataBatchReader
from the batch request header. For Content-Type ofapplication/json
with optional parameters, an instance ofODataJsonLightBatchReader
is created, while anODataMultipartMixedBatchReader
object is created forMultipart/Mixed
content type with optional parameters.ODataMessageWriter batchResponseMessageWriter = new ODataMessageWriter(odataResponseMessage, odataMessageWriterSettings, model); ODataBatchWriter batchWriter = batchResponseMessageWriter.CreateODataBatchWriter();
Similar to the request message writer, the response message writer above also sets up the proper
ODataPayloadKind
for Batch processing.Now, start the basic batch processing with request reading / response writing. For batch processing, the request processing (including reading, dispatching) and response writing (including processing responses for individual request, writing batch response) are interleaving and the whole process is driven by OData service using specific call patterns as described below (common for both formats).
Typically odataBatchReader is started with initial state, so we can start with creating the response envelop, followed by readings & writings.
batchWriter.WriteStartBatch(); while (batchReader.Read()) { switch (batchReader.State) { case ODataBatchReaderState.Operation: // Create operation request message ODataBatchOperationRequestMessage operationRequestMessage = batchReader.CreateOperationRequestMessage(); // Create operation response message IODataResponseMessage responseMessage = batchWriter.CreateOperationResponseMessage(operationRequestMessage.currentContentId); // Request processing begins: including request body reader creation, request parsing & execution according to dependencies specified etc. belonged // to implementation of specific services. if (operationMessage.Method == "PUT") { // Processing request payload. using (ODataMessageReader operationMessageReader = new ODataMessageReader( operationMessage, new ODataMessageReaderSettings(), this.userModel)) { ODataReader reader = operationMessageReader.CreateODataResourceReader(); // Reader reads data from request payload and kick off service backend processing. ... } // Query various attributes for this operation request such as request dependencies and atomic group associated // and combine with specific processing of the service. MyProcessingForExecutionOrders(operationMessage.DependsOnIds); MyProcessingForAtomicTransaction(operationMessage.GroupId); // ... // Response processing begins: including response writing (depends on whether it is associated with an atomic group) etc. to // write back to the HTTP response belonged to implementation of specific services. response.StatusCode = 201; response.SetHeader("Content-Type", "application/json;odata.metadata=none"); ... // Response processing ends. } else if (operationMessage.Method == "PATCH") { // Process flow of PATCH method similar to PUT method above. // ... } // additional else if blocks for other HTTP method processs flow come here... break; case ODataBatchReaderState.ChangesetStart: // Request's GroupId can be passed to response writer for correlation of groupId between request & response. batchWriter.WriteStartChangeset(batchReader.CurrentGroupId); // Handle atomic group starting here as needed by implementation of specific services. ... // End of atomic group start processing. break; case ODataBatchReaderState.ChangesetEnd: batchWriter.WriteEndChangeset(); // Handle atomic group ending here as needed by implementation of specific services. ... // End of atomic group end processing. break; } } batchWriter.WriteEndBatch();
With the introduction of JSON batch, the following public APIs / attributes are available:
ODataBatchWriter
:public ODataBatchOperationRequestMessage CreateOperationRequestMessage(string method, System.Uri uri, string contentId, BatchPayloadUriOption payloadUriOption, IEnumerable<string> dependsOnIds)
: This new overload can be used to create operation request message with the additional dependsOnIds that explicitly specify operation dependencies in a JSON batch request. This new overload can also be used for Multipart/Mixed to validate the dependencies. Calling an overload without dependencyIDs in Multipart/Mixed validates any referenced statement results are valid according to the protocol.public void WriteStartChangeset(string)
: This new overload can be used to specify the atomic group Id when the operation request is created, or the operation response’s atomic group Id correlating to value in the request as required by the OData JSON Batch semantics.
ODataBatchReader
:public string CurrentGroupId
: This returns the current group Id when the atomic group is created. It can be used to create the same group Id on the batch response corresponding to that in the incoming request, as required by the OData JSON Batch semantics. Please note that this is not the case for Multipart/Mixed format where the changeset boundary string in request and response are not correlated.
ODataBatchOperationRequestMessage
:public string GroupId
: This returns the operation request’s group Id. For JSON batch, this is the value specified by the user; For Multipart/Mixed, this is the Id string used to generated the boundary parameter value of the changeset.public List<string> DependsOnIds
: This returns a list of prerequisite request ids of this operation request.- For JSON batch, if the user-specified DependsOn data contains atomic group Id, the atomic group Id will be flattened into its contained required Ids.
- For Multipart/Mixed batch, the list is default to be derived implicitly per the protocol for Multipart/Mixed format in the original APIs of
ODataBatchWriter.CreateOperationRequestMessage
, but it can also be specified explicitly by the user using the new overloadedODataBatchWriter.CreateOperationRequestMessage
API above.
Notes:
- Only APIs of synchronous flavor are listed above. For async flavor please refer to the corresponding synchronous API.
- Atomic group in JSON batch corresponds to changeset in Multipart/Mixed batch.
Typical JSON Batch Response Processing using OData-Core library
The processing of JSON batch response is similar to that of Multipart/Mixed batch, as shown in the following code snippet:
ODataBatchOperationResponseMessage operationMessage = batchReader.CreateOperationResponseMessage(); using (ODataMessageReader innerMessageReader = new ODataMessageReader( operationMessage, new ODataMessageReaderSettings(), this.userModel)) { ODataReader reader = innerMessageReader.CreateODataResourceReader(); while (reader.Read()) { // resource data processing here... } }
Note the using block for inner message reader, which helps disposal of the body content stream of the request created during the response reading. The creation of the response body content stream during response processing, similar to creation of the request body content stream during request processing, enables the parallel processing or dispatching of responses or requests of JSON batch.
-
IN Operator
Overview
The IN operator is a new feature in OData 4.01 that enables a shorthand way of writing multiple EQ expressions joined by OR. For example,
GET /service/Products?$filter=Name eq 'Milk' or Name eq 'Cheese' or Name eq 'Donut'
can become
GET /service/Products?$filter=Name in ('Milk', 'Cheese', 'Donut')
Of the binary expression invoking IN, the left operand must be a single value and the right operand must be a comma-separated list of primitive values or a single expression that resolves to a collection; the expression returns true if the left operand is a member of the right operand.
Usage
The
IN
operator is used in expressions that resolve to a boolean. Common use would be with$filter
and it can also be used for$orderby
. See test cases for examples of supported scenarios.Remarks
Having successfully parsed the
$filter
expression, theFilterClause
will have anExpression
member which can be casted to anInNode
object. The members of InNodeLeft
andRight
representSingleValueNode
andCollectionNode
respectively. From there, it is up to the user to do whatever they would like with the parsed information from theInNode
. As shown in the code samples, it may be in the user’s best interest to downcast theSingleValueNode
andCollectionNode
to subclasses to access additional properties. -
Resource (Complex & Entity) Value
1. Introduction
Abstract
ODataComplexValue
is widely used in OData libraries v5.x and v6.x. However, it’s removed in OData library v7.x because complex type should support the navigation property. We should treat the complex same as the entity.So, the main changes OData v7.x design are:
- Remove ODataComplexValue
- Rename ODataEntry as ODataResource, use that to represent the instance of entity and complex.
- Rename ODataFeed as ODataResourceSet, use that to represent the instance of collection of entity or complex.
Problems
Along with more and more customers upgrade from ODL v6.x to ODL v7.x, many customers find it’s hard to use the library without the
ODataComplexValue
. Because most of OData customers:- Don’t need navigation property on complex type.
- Can’t convert the instance of entity or complex easily from literal or to literal. Like the Json.Net
- Can’t create the instance annotation with “complex or entity, or collection of them”
- Can’t write or read the top-level resource
- Can’t write or read the top-level property with resource or collection of resource value
- Can’t read the parameter resource or collection of resource value directly
2. Proposal
Current structure
Below is the main inheritance of the ODataValue vs ODataItem in ODL v6.x.
Below is the main inheritance of the ODataValue vs ODataItem in ODL v7.x.
The main changes from 6.x to 7.x is:
ODataComplexValue
is removed.ODataValue
is derived fromODataItem
.
Proposal structure
We should introduce a new class named
ODataResourceValue
derived fromODataValue
, same as:3. Main Works
ODataResourceValue
classWe will introduce a new class named
ODataResourceValue
, it should look like (Same asODataComplexValue
):Where:
- TypeName: save the resource type name.
- Properties: save the all properties, include the property with resource value or collection of resource value.
- InstanceAnnotations: save the instance annotations for this resource value.
ODataCollectionValue
classWe don’t need to change anything for
ODataCollectionValue
, because it also supports to haveODataResourceValue
as its element.ODataProperty
classWe don’t need to change anything for
ODataProperty
, because it also supports to createODataProperty
withODataResourceValue
orODataCollectionValue
.ODataResource
classWe don’t need to change anything in
ODataResource
except to verify the properties don’t include any property whose value is anODataResourceValue
or Collection of ODataResourceValue.ODataResourceSet
classWe don’t need to change anything in
ODataResourceSet
.Write
ODataResourceValue
Write in value writer
Write
ODataResourceValue
We should add a new method named
WriteResourceValue(…)
to write anODataResourceValue
in ODataJsonLightValueSerializer.This method is the key method in writing scenario.
Write
ODataCollectionValue
withODataResourceValue
We should update
WriteCollectionValue(...)
method to call aboveWriteResourceValue(...)
if the item is anODataResourceValue
.Write in property Writer
We should update ODataJsonLightPropertyWriter.cs to support writing the property with
ODataResourceValue
or collection of resource value. This change should inWriteProperty(...)
method, it supports to write top-level property and non-top-level property (Nested Property).We don’t need to change any codes for property with value as
ODataCollectionValue
which element isODataResourceValue
.Write in instance annotation writer
We should update JsonLightInstanceAnnotationWriter.cs to support:
Write the
ODataInstanceAnnotation
with value asODataResourceValue
We should add the below codes in
WriteInstanceAnnotation(...)
method by calling theWriteResourceValue(...)
if the instance annotation value is anODataResourceValue
.Write the
ODataInstanceAnnotation
with value as collection ofODataResourceValue
.We don’t need to do anything. Because it supports to write the
ODataCollectionValue
, which in turns will callWriteResourceValue()
for eachODataResourceValue
elements.Write in collection writer
We should update ODataJsonLightCollectionWriter.cs to support write collection with
ODataResourceValue
item.Write in parameter writer
We should update ODataJsonLightParameterWriter.cs to support write resource or collection of resource value.
TBD: Normally, if you want to write a Collection parameter, you should do:
However, i think we should support to write the collection value directly if customer call
WriteValueParameter()
method with theODataCollectionValue
.Basically, we don’t need to change any codes for the “Collection parameter value” writer. Customer still can use “CreateCollectionWriter” to write the collection with more information.
Besides, We don’t need to change any codes for Resource or ResourceSet parameter writer. Customer still can use them to writer
ODataResource
orODataResourceSet
one by one. See:- CreateFormatResourceWriter
- CreateFormatResourceSetWriter
Convert
ODataResourceValue
to Uri literalWe should support to convert
ODataResourceValue
and collection of it to Uri literal when customer callConvertToUriLiteral(...)
in ODataUriUitl.cs.Read
ODataResourceValue
Read ODataResourceValue in value reader
We should update ODataJsonLightPropertyAndValueDeserialier.cs to read the resource value.
This method is the key method in reading scenario, it should support to:
- Read its own instance annotation
- Read all properties value, include nested resource value.
The above method is called from:
For the collection of resource, owing that
ReadCollectionValue()
will callReadNonEntityValueImplemenation(…)
to read its elements, so, if the item isentity or complex
, it will returnDataResourceValue
. We don’t need to change any codes.Read ODataResourceValue in instance annotation reader
ODataJsonLightPropertyAndValueDeserialier.cs has the following method to read instance annotation value:
So, we don’t need to change any codes for it.
Read ODataResourceValue in collection reader
ODataJsonLightCollectionDeserializer.cs will call
ReadNonEntityValueImplementation
. We don’t need change any code.However, there’s some validation codes that need to change.
Read ODataResourceValue as OData error value
ODataJsonLightErrorDeserializer.cs will call
ReadNonEntityValueImplementation
. We don’t need change any code.Read in Resource deserializer
ODataJsonLightResourceDeserializer.cs will call
ReadNonEntityValueImplementation
to read the value of its property. However,- It is not used to read the “complex and collection of complex”
- It is not used to read the “navigation property”
- It’s ONLY used it to read the primitive, enum and collection of them. And, for the “complex and collection of complex”, we still create nested resource info. So, we don’t need to change anything.
Read resource in parameter
ODataJsonLightParameterDeserialzer.cs is used to read parameter value. So far, for the entity, complex, it only returns a parameter state as “Resource”, for the collection of them, return a parameter state of “ResourceSet” as below:
- Primitive type, read as primitive value.
- Enum type, read as “ODataEnumValue”
- TypeDefintion, read as “TypeDefinition” value
- Complex, Entity, read nothing, just return “ReaderState.Resource”
- Collection,
- If element is primitive, read as primitive value.
- If element is enum, read nothing, just return “ReaderState.Collection”
- If element is complex, entity, read nothing, just return “ReaderState.ResourceSet”. So, we should have a configuration enable customer to change the logic. For example: On ODataMessageReader, we can enable customer to create a parameter reader which can read all parameter as value.
So, if customer call the above method using true for
readAllAsValue
, he can get:- Complex, Entity, read as “ODataResourceValue”
- Collection, read as “ODataCollectionValue”.
Read Top-Level Property
ODataJsonLightPropertyAndValueDeserializer.cs can read the top-level property into a ODataProperty. So, we can read a top-level complex, entity, or collection or complex, entity property.
Convert ODatResourceValue from Url literal
We should convert the
ODataResourceValue
from JSON Uri literal inConvertFromUriLiteral(...)
in ODataUriUtils.cs.4. Open Questions
What’s the string output if convert a null “ODataCollectionValue”?
So far, if you create:
However, it should be “[]” ?
Do we write the instance annotation if call
ConvertToUriLiteral
?In the 6.x version, if a complex value has instance annotations, those instance annotations will not write out when we call like:
Where,
str
doesn’t include the instance annotation? But, we should include the instance annotation.
6. DESIGN
-
6.1 IN Operator Design
IN Operator
OData Specification
The IN operator is a new feature in OData 4.01 that enables a shorthand way of writing multiple EQ expressions joined by OR. For example,
GET /service/Products?$filter=Name eq 'Milk' or Name eq 'Cheese' or Name eq 'Donut'
can become
GET /service/Products?$filter=Name in ('Milk', 'Cheese', 'Donut')
Of the binary expression invoking IN, the left operand must be a single value and the right operand must be a comma-separated list of primitive values or a single expression that resolves to a collection; the expression returns true if the left operand is a member of the right operand.
Syntax
Augmented Backus-Naur Form (ABNF)
Per the ABNF, the IN operator is part of the common expression syntax
commonExpr = ( primitiveLiteral / arrayOrObject / rootExpr / firstMemberExpr / functionExpr / negateExpr / methodCallExpr / parenExpr / listExpr / castExpr / isofExpr / notExpr ) [ addExpr / subExpr / mulExpr / divExpr / divbyExpr / modExpr ] [ eqExpr / neExpr / ltExpr / leExpr / gtExpr / geExpr / hasExpr / inExpr
Test cases explicitly outlined by the ABNF spec include the following (see Scenarios for details on some of these types of queries):
<TestCase Name="5.1.1.1.12 Logical Operator Examples" Rule="boolCommonExpr"> <Input>Name in ('Milk', 'Cheese')</Input> </TestCase> <TestCase Name="5.1.1.1.12 Logical Operator Examples" Rule="boolCommonExpr"> <Input>Name in ["Milk", "Cheese"]</Input> </TestCase>
Design Strategy
Uri Query Expression Parser
The logic to parse expressions for query options are written in the UriQueryExpressionParser class (
src\Microsoft.OData.Core\UriParser\Parsers\UriQueryExpressionParser.cs
). The algorithm tokenizes the expression by whitespaces and builds QueryTokens from the operands. If more than one operand exists, then the algorithm recursively builds the right operand as a tree.There exists a code flow for generating binary operator tokens (used for eq, ne, gt, ge, has, etc.), which would make sense for IN to follow. However, the binary operator tokens are eventually converted to binary operator nodes, where the left and right nodes must be single value nodes; the binary operator node does not suit the IN operator, which has a left single-valued operand and right collection-valued operand. Therefore, we will create separate QueryToken, MetadataBinder, and SingleValueNode classes to accommodate IN but maintain a similar code flow as the binary operator.
Additionally, we will need to ensure that the expression parser can read static collections (i.e. parentheses-enclosed members), so we will derive from the CollectionNode class to represent the static collection.
QueryToken Class
We will need to derive a separate class for IN scenarios. The code path looks similar to the BinaryOperatorToken construction but the algorithm will result in an InToken, which holds different information from the BinaryOperatorToken. See
src\Microsoft.OData.Core\UriParser\Parsers\UriQueryExpressionParser.cs : ParseComparison()
for code path./// <summary> /// Base class for all lexical tokens of OData query. /// </summary> public abstract class QueryToken { /// <summary> /// Empty list of arguments. /// </summary> public static readonly QueryToken[] EmptyTokens = new QueryToken[0]; /// <summary> /// The kind of the query token. /// </summary> public abstract QueryTokenKind Kind { get; } /// <summary> /// Accept a <see cref="ISyntacticTreeVisitor{T}"/> to walk a tree of <see cref="QueryToken"/>s. /// </summary> /// <typeparam name="T">Type that the visitor will return after visiting this token.</typeparam> /// <param name="visitor">An implementation of the visitor interface.</param> /// <returns>An object whose type is determined by the type parameter of the visitor.</returns> public abstract T Accept<T>(ISyntacticTreeVisitor<T> visitor); }
SingleValueNode Class
The InNode will derive from the SingleValueNode class and will have similarities with the BinaryOperatorNode. The InNode will have a SingleValueNode and CollectionNode.
/// <summary> /// Base class for all semantic metadata bound nodes which represent a single composable value. /// </summary> public abstract class SingleValueNode : QueryNode { /// <summary> /// Gets the type of the single value this node represents. /// </summary> public abstract IEdmTypeReference TypeReference { get; } /// <summary> /// Gets the kind of this node. /// </summary> public override QueryNodeKind Kind { get { return (QueryNodeKind)this.InternalKind; } } }
Static Collections with Parentheses/Brackets
The expression parser must also recognize static collections enclosed by parentheses or brackets. In these instances, the parser must create a collection with such objects. Therefore we will create a new CollectionConstantNode, derived from CollectionNode, that represents a list of ConstantNodes.
/// <summary> /// Base class for all semantic metadata bound nodes which represent a composable collection of values. /// </summary> public abstract class CollectionNode : QueryNode { /// <summary> /// The resouce type of a single item from the collection represented by this node. /// </summary> public abstract IEdmTypeReference ItemType { get; } /// <summary> /// The type of the collection represented by this node. /// </summary> public abstract IEdmCollectionTypeReference CollectionType { get; } /// <summary> /// Gets the kind of this node. /// </summary> public override QueryNodeKind Kind { get { return (QueryNodeKind)this.InternalKind; } } }
-
6.2 Design doc for $expand in context-url for OData V4.01
$expand in Context Url
Introduction
In form of metadata, context url in response from OData-compliant service provides a way to describe the response payload, and is used as control information to facilitate response processing by the client.
Here is an example of a simple query request and resulting response containing context url:
Request url: http://services.odata.org/V4/TripPinService/People(‘russellwhyte’)?$select=FirstName,LastName
Response:
{ "@odata.context": "http://services.odata.org/V4/TripPinService/$metadata#People(FirstName,LastName)/$entity", "@odata.id": "http://services.odata.org/V4/TripPinService/People('russellwhyte')", "@odata.etag": "W/\"08D62407F53DE9ED\"", "@odata.editLink": "http://services.odata.org/V4/TripPinService/People('russellwhyte')", "FirstName": "Russell", "LastName": "Whyte" }
As shown above, @odata.context annotation specifies the context url, and provides the following machine-readable description for response data:
- It is regarding an entity from the “People” entity set,
- It contains two property values “FirstName”, “LastName”.
Expanded Entity Specification for OData V4.01
OData V4.0 specifies that expanded entities with nested selects are included in the context Url as the name of the navigation property suffixed with the comma separated list of selected properties, enclosed in parens. OData V4.01 format specifies that, in the absence of any nested selects, the expanded navigation property appears suffixed with empty parens. This is distinct, and may appear in addition to, the un-suffixed navigation property name, which indicates that the navigation property appears in the $select list (indicating that the navigationLink should be included in the response).
10.10 Expanded Entity Context URL template: {context-url}#{entity-set}{/type-name}{select-list}/$entity {context-url}#{singleton}{select-list} {context-url}#{type-name}{select-list} For a 4.01 response, if a navigation property is explicitly expanded, then in addition to the non-suffixed names of any selected properties, navigation properties, functions or actions, the comma-separated list of properties MUST include the name of the expanded property, suffixed with the parenthesized comma-separated list of any properties of the expanded navigation property that are selected or expanded. If the expanded navigation property does not contain a nested $select or $expand, then the expanded property is suffixed with empty parentheses. [If the expansion is recursive for nested children, a plus sign (+) is infixed between the navigation property name and the opening parenthesis.] For a 4.0 response, the expanded navigation property suffixed with parentheses MAY be omitted from the select-list if it does not contain a nested $select or $expand, but MUST still be present, without a suffix, if it is explicitly selected.
Change Summary
According to the OData spec above, this change is for creating proper expand token in response’s context url, corresponding to $expand clause in the request url. Context Url is used as response metadata to control data materialization when response is received by the client. We need to make sure that:
- Context url in the response generated contains correct token for the $expand clause.
- Client can parse the context url correctly and uses the metadata for response processing.
V4.01
As required, we are going to add to the context url the parenthesized (either empty or non-empty) navigation property when it is expanded. For example:
Request Url Context Url in Response root/Cities(‘id’)?$expand=TestModel.CapitalCity/Districts root/$metadata#Cities(TestModel.CapitalCity/Districts())/$entity root/Cities(‘id’)?$expand=TestModel.CapitalCity/Districts($select=DistrictName) root/$metadata#Cities(TestModel.CapitalCity/Districts(DistrictName))/$entity root/Cities(‘id’)?$select=Name&$expand=TestModel.CapitalCity/Districts($select=DistrictName) root/$metadata#Cities(Name,TestModel.CapitalCity/Districts(DistrictName))/$entity root/Cities(‘id’)?$select=Name,TestModel.CapitalCity/Districts&$expand=TestModel.CapitalCity/Districts($select=DistrictName) root/$metadata#Cities(Name,TestModel.CapitalCity/Districts,TestModel.CapitalCity/Districts(DistrictName))/$entity We also fix the known issue https://github.com/OData/odata.net/issues/1104 when a navigation link is both expanded and selected, the parenthesized navigation property will be present in the context-url, along with the navigation link explicitly selected:
Request Url Context Url in Response root/Cities(‘id’)?$select=TestModel.CapitalCity/Districts&$expand=TestModel.CapitalCity/Districts root/$metadata#Cities(TestModel.CapitalCity/Districts,TestModel.CapitalCity/Districts())/$entity root/Cities(‘id’)?$select=Id,Name,ExpandedNavProp&$expand=ExpandedNavProp root/$metadata#Cities(Id,Name,ExpandedNavProp,ExpandedNavProp())/$entity V4.0
We are going to tweak the behavior to align with that of the 4.01. While this change does introduce slightly different semantics for explicitly selected navigation property, it is not considered a breaking change because the updated context-url form for expanded navigation link is legal, but just was not required. It has been confirmed through code examination and ODL tests that this change doesn’t cause anomalies for libraries and client.
Context Url Parsing Updates to Align with OData V4/V4.01 ABNF Spec
Notion of expanded entity in context url has been introduced since OData V4 ABNF. Here is the excerpt of context url select clause in OData V4.01 ABNF:
;------------------------------------------------------------------------------ ; 3. Context URL Fragments ;------------------------------------------------------------------------------ context = "#" contextFragment contextFragment = 'Collection($ref)' / '$ref' / 'Collection(Edm.EntityType)' / 'Collection(Edm.ComplexType)' / singletonEntity [ navigation *( containmentNavigation ) [ "/" qualifiedEntityTypeName ] ] [ selectList ] / qualifiedTypeName [ selectList ] / entitySet ( '/$deletedEntity' / '/$link' / '/$deletedLink' ) / entitySet keyPredicate "/" contextPropertyPath [ selectList ] / entitySet [ selectList ] [ '/$entity' / '/$delta' ] entitySet = entitySetName *( containmentNavigation ) [ "/" qualifiedEntityTypeName ] containmentNavigation = keyPredicate [ "/" qualifiedEntityTypeName ] navigation navigation = *( "/" complexProperty [ "/" qualifiedComplexTypeName ] ) "/" navigationProperty selectList = OPEN selectListItem *( COMMA selectListItem ) CLOSE selectListItem = STAR ; all structural properties / allOperationsInSchema / [ qualifiedEntityTypeName "/" ] ( qualifiedActionName / qualifiedFunctionName / selectListProperty ) selectListProperty = primitiveProperty / primitiveColProperty / navigationProperty [ "+" ] [ selectList ] / selectPath [ "/" selectListProperty ] contextPropertyPath = primitiveProperty / primitiveColProperty / complexColProperty / complexProperty [ [ "/" qualifiedComplexTypeName ] "/" contextPropertyPath ]
The expanded entity introduced nested list of comma-separated items for the projected expanded entity. While expanded entity is supported at high level per OData V4 Spec, some common cases such as projecting multiple properties in expanded entities, e.g. $expand=Account($select=UPN,DisplayName), are not supported by context Url parsing implementation, which currently is aligned with only ABNF V3.
This requires updating the context URL selected clause parsing implementation to align with ABNF V4.01. Issue #1268 is added to the scope of this task.
Design
-
In OData 6.x & 7.x implementation, while creation of the context url select clause is aligned with OData V4/V4.01 ABNF spec, its counterpart of lexical parsing is still using OData ABNF V3, which specifies context url select list as a flat list of comma-separated items. To align with OData ABNF V4.01, parser needs to adopt a recursive approach. In addition to the path segments representing simple properties originally, a notion of expanded entity needs to be introduced in SelectedPropertiesNode to accommodate requirements from OData ABNF V4.
-
During response de-serialization, the emitted expanded navigation property token (in the form of parenthesized navigation link) could cause incompatibility/ambiguity to the semantics of context-url’s selected-list, where an empty list stands for select-all(entire subtree). This is actually an issue in current library which always emits “Projected Expanded Entity” into the selected list. It could cause inadvertent omission of navigation link information during materialization (see issue #1259) in current library, and needs to be addressed as a prerequisite of this task. . Check whether the select clause during SelectPropretiesNode instantiation only contains expanded navigation link, which contains balanced parentheses. The item should also be resolvable to navigation property, not other types of property. . Additional EDM type information needs to be passed in for navigation property resolution in the constructor of the SelectedPropertiesNode. The EDM type information is originated from the OData reader, which instantiates the SelectedPropertiesNode.
-
For both V4 and V4.01, always emit the parenthesized navigation link during response writing for context-url.
-
For internal logic of combining select list and expand list together, we should, according to select list’s semantics, create a node of entire subtree when the selected list is empty.
-
Cleanup the callback function processSubResult signatures by removing ODataVersion argument and associated method, since same logic is used for both V4 and V4.01. The callback function is used for traversing the select&expand clause recursively and providing aggregated result.
private static string ProcessSubExpand(string expandNode, string subExpand, ODataVersion version)
private static SelectedPropertiesNode ProcessSubExpand(string nodeName, SelectedPropertiesNode subExpandNode, ODataVersion version)
Work Items Breakdown:
Task | Estimate ——| ——– Research on OData spec for v4.01 related to context-url and projected expand entity | Completed Research on OData implementation related to context-url serialization / de-serialization / metadata level | Completed Address prerequisite issue of navigation link materialization for expand-sub-select clause combined with minimal metadata | Completed (Code Review pending) Design documentation | Completed Implementation of $expand in context url for V4.01: based on PoC done during research | 3 days (WIP, pending on Design Review) Implementation of context url select clause parsing per OData V4.01 ABNF spec. Feature doc | 1 day
Note: The above estimates include test pass for the initial implementation. Code review cycles are not included.
Tracking:
- Prerequisite issue/PR: https://github.com/OData/odata.net/issues/1259; https://github.com/OData/odata.net/pull/1264
- “$expand for context-url” issue/PR: https://github.com/OData/odata.net/issues/1265;