原文来自: http://odata.github.io/odata.net/
1. CORE
-
1.1 Write OData Payload
There are several kinds of OData payload, includes service document, model metadata, feed, entry, entity references(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, we’ll set up the neccessary code that is common to all kind of payload.Class ODataMessageWriter is the entrance class to write the OData Payload.To construct an ODataMessageWriter instance, you’ll need to provide an IODataResponseMessage, or IODataRequestMessage, depends 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 scenario.In this tutoria, 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.
Create the settings:
Now we are ready to create the ODataMessageWriter instance:
After we write the payload, we can inspect into the memory stream wrapped in InMemoryMessage to check what is written.
Here is the whole program that use SampleModelBuilder and InMemoryMessage to write metadata payload:
Now we’ll go through on each kind of payload.
Write metadata
Write metadata is simple, just use WriteMetadataDocument method in ODataMessageWriter.
Please be noticed that this API only works when: 1. Writting response message, that means when constructing the ODataMessageWriter, you mut supply IODataRequestMessage. 2. 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 a ODataServiceDocument instance, which will contains all the neccessary information in a service document, that include, entity set, singleton and function import.
In this example, we create a service document that contains two entity sets, one singleton and one function import.
Then let’s call WriteServiceDocument method to write it.
However, this would not work. An ODataException will threw up said that “The ServiceRoot property in ODataMessageWriterSettings.ODataUri must be set when writing a payload.” This is because a valid service document will contains a context url reference to the metadata url, which need to be told in ODataMessageWriterSettings.
This service root informaiton is provided in ODataUri.ServiceRoot, as this code shows.
As you can see, you don’t need to provide model to write service document.
It is a little work to instantiate the service document instance and set up the entity sets, singletons and function imports. Actually, the EdmLib provided a useful API which can generate a service document instance from model. The API is named GenerateServiceDocument, and defined as an extension method on IEdmModel.
All the entity sets, singletons and function imports whose IncludeInServiceDocument attribute is set to true in the model will be in the generated service document. And according to the spec, only those function import without any parameter should set its IncludeInServiceDocument attribute to true.
And as WriteMetadata API, WriteServiceDocument works only when it is writing a response message.
Besides API WriteServiceDocument, there is another API called WriteServiceDocumentAsync in ODataMessageWriter class. It is an async version of WriteServiceDocument, so you can call it in async way.
A lot of API in writer and reader provides async version of API, they all work as a async complement of the API that without Async suffix.
Write Feed
Collection of entities is called feed in OData Core Library. Unlike metadata or service document, you must create another writer on ODatMessageWriter to write the feed. The library is designed to write feed in an streaming way, which means the entry is written one by one.
Feed is represented by ODataFeed class. To write a feed, following information are needed: 1. The service root, which is defined by ODataUri. 2. The model, as construct parameter of ODataMessageWriter. 3. Entity set and entity type information.
Here is how to write an empty feed.
Line 4 give the service root, line 6 give the model, and line 10 give the entity set and entity type information.
The output of it looks like this.
The output contains a context url in the output, which is based on the service root you provided in ODataUri, and the entity set name. There is also a value which is an empty collection, where will hold the entities if there is any.
There is another way to provide the entity set and entity type information, through ODataFeedAndEntrySerializationInfo, and in this no model is needed.
When writting feed, you can provide a next page, which is used in server driven paging.
The output will contains a next link before the value collection.
If you want the next link to be appear after the value collection, you can set the next link after the WriteStart call, before the WriteEnd call.
There is no rule on next link, as long as it is a valid url.
To write entry in the feed, create the ODataEntry instance and call WriteStart and WriteEnd on it between the WriteStart and WriteEnd call of feed.
We’ll introduce more details on writting entry in next section.
Write Entry
Entry can be written in several places: 1. As the top level entry. 2. As the entry in a feed. 3. As the entry expanded an other entry.
To write a top level entry, use ODataMessageWriter.CreateEntryWriter.
We’ve already introduced how to write entry in a feed in last section, now we’ll look at how to write entry expanded in another entry.
The output will contains order entity inside the customer entity.
-
1.2 Read OData Payload
The reader API is almost like the writer API, so you can expect the symmetry here.First, we’ll set up the neccessary code that is common to all kind of payload.Class ODataMessageReader is the entrance class to read the OData Payload.To construct an ODataMessageReader instance, you’ll need to provide an IODataResponseMessage, or IODataRequestMessage, depends 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 scenario.In this tutoria, 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.
Create the settings:
Now we are ready to create the ODataMessageReader instance:
We’ll use the code in the first part to write the payload, and in this section use the reader to read the payload. After write the payload, we should set the Position of MemoryStream to zero.
Here is the whole program that use SampleModelBuilder and InMemoryMessage to first write then read metadata payload:
Now we’ll go through on each kind of payload.
Read metadata
Read metadata is simple, just use ReadMetadataDocument method in ODataMessageReader.
Just like writing metadata, this API only works when reading response message, that means when constructing the ODataMessageReader, you must supply IODataResponseMessage.
Read service document
Read service document is through the ReadServiceDocument API.
And as ReadMetadata API, ReadServiceDocument works only when it is reading a response message.
Besides API ReadServiceDocument, there is another API called ReadServiceDocumentAsync in ODataMessageReader class. It is an async version of ReadServiceDocument, so you can call it in async way.
Read Feed
To read a feed, you must create another reader on ODataFeedReader to read the feed. The library is designed to read feed in an streaming way, which means the entry is read one by one.
Here is how to read a feed.
Read Entry
To read a top level entry, use ODataMessageReader.CreateEntryReader. Other than that, there is no different compared to read feed.
-
1.3 Use ODataUriParser
This post is intended to guide you through the UriParser for OData V4, which is released within ODataLib V6.0 and later.You may have already read the following posts about OData UriParser in ODataLib V5.x: - Parsing $filter and $orderby using the ODataUriParser
- Parsing OData Paths, $select and $expand using the ODataUriParser Some parts of the articles still apply to V4 UriParser, such as introduction for ODataPath and QueryNode hierarchy. In this post, we will deal with API changes and features newly introduced.
UriParser Overview
The main reference document for UriParser is the URL Conventions specification. The ODataUriParser class is its main implementation in ODataLib.
The ODataUriParser class has two main functionalities:
- Parse resource path
- Parse query options
We’ve also introduced the new ODataQueryOptionParser class in ODataLib 6.2+, in case you do not have the full resource path and only want to parse the query options only. The ODataQueryOptionParser shares the same API signature for parsing query options, you can find more information below.
Using ODataUriParser
The use of ODataUriParser class is easy and straightforward, as we mentioned, we do not support static methods now, we will begin from creating an ODataUriParser instance.
ODataUriParser has only one constructor:
Parameters:
model is the Edm model the UriParser will refer to; serviceRoot is the base Uri for the service, which could be a constant for certain service. Note that serviceRoot must be an absolute Uri; fullUri is the full request Uri including query options. When it is an absolute Uri, it must be based on the serviceRoot, or it can be a relative Uri. In the following demo we will use the model from OData V4 demo service , and create an ODataUriParser instance.
Parsing Resource Path
You can use the following API to parse resource path:
You don’t need to pass in resource path as parameter here, because the constructor has taken the full Uri.
The ODataPath holds the enumeration of path segments for resource path. All path segments are represented by classes derived from ODataPathSegment.
In our demo, the resource Path in the full Uri is Products(1), then the result ODataPath would contain two segments: one EntitySetSegment for EntitySet named Products, and the other KeySegment for key with integer value “1” .
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 an instance of class XXXClause, which represents the query option as an Abstract Syntax Tree (with semantic information bound). Note that $select and $expand query options are merged together in one SelectExpandClause class. The latter three all have primitive type value, and the parsing result is the corresponding primitive type wrapped by Nullable class.
For all query option parsing results, the Null value indicates the corresponding query option is not specified in the request URL.
Here is a demo for parsing the Uri with all kinds of query options (please notice that value of skip would be null as it is not specified in the request Uri) :
The data structure for SelectExpandClause, FilterClause, OrdeyByClause have already been presented in the two previous articles mentioned at the top of this post. Here I’d like to talk about the newly introduced SearchClause.
SearchClause contains tree representation of the $search query. The detailed rule of $search query option can be found here. In general, the search query string can contain search terms combined with logic keywords: AND, OR and NOT.
All search terms are represented by SearchTermNode, which is derived from SingleValueNode. SearchTermNode has one property named Text, which contains the original word or phrases.
SearchClause’s Expression property holds the tree structure for $search. If the $search contains single word, the Expression would be set to that SearchTermNode. But when $search is a combination of various term and logic keywords, the Expression would also contains nested BinaryOperatorNode and UnaryOperatorNode.
For example, if the query option $search has the value “a AND b”, the result expression (syntax tree) would have the following structure:
Using ODataQueryOption Parser
There may be some cases that you already know the query context information but does not have the full request Uri. The ODataUriParser does not seems to be available as it will always require the full Uri, then the user would have to fake one.
In ODataLib 6.2 we shipped a new Uri parser that targets at query options only, it requires the model and type information be provided through its constructor, then it could be used for query options parsing as same as ODataUriParser.
The constructor looks like this:
Parameters (here the target object indicates what resource path was addressed, see spec):
model is the model the UriParser will refer to; targetEdmType is the type query options apply to, it is the type of target object; targetNavigationSource is the EntitySet or Singleton where the target comes from, it is usually the NavigationSource of the target object; queryOptions is the dictionary containing the key-value pairs for query options.
Here is the demo for its usage, it is almost the same as the ODataUriParser:
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) as well as APIs to read (or write) an entity data model from (or to) a CSDL document.This section shows how to build a basic entity data model using EdmLib APIs. Software Versions Used in the Tutorial
Create the 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. 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
clauses to introduce the EDM definitions:Then replace the boilerplate code with the following:
Add a 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 multiple members can be selected simultaneously; - Adds three enumeration members
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
of the namespaceSample.NS
; - Adds the container to the model.
Note that each model MUST define exactly one entity container (aka. the
DefaultContainer
) which can be referenced later by 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.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 would write it to a CSDL document.
In the Program.cs file, add the following
using
clauses:Then replace the boilerplate
Program
class with the following:For now, there is no need to understand how the model is being written as 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 other XML viewer if you like). 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 continue to use and extend the sample from the previous section.Using the CsdlWriter APIs
We have already used one of the APIs to write the model to a CSDL document in the last section.
The
CsdlWriter.TryWriteCsdl()
method is defined as an extension method toIEdmModel
:The second parameter
writer
requires anXmlWriter
which can be created through the overloadedXmlWriter.Create
methods. Remember to either apply ausing
clause to anXmlWriter
instance or explicitly callXmlWriter.Flush()
(orXmlWriter.Close()
) to flush the buffer to its underlying stream. The third parametererrors
is used to pass out the errors found when writing the model. If the method returnstrue
(indicating write success), theerrors
should be an emptyEnumerable
; otherwise it contains all the model errors.The other version of the
CsdlWriter.TryWriteCsdl()
method is:This overload is called when the model to write contains referenced models. The referenced models need to be written into separate files. So the second parameter
writerProvider
takes a callback to create a differentXmlWriter
for each referenced model where thestring
parameter is the schema namespace of that model. A simplewriterProvider
would be:Using the CsdlReader APIs
The
CsdlReader
APIs are defined as follows:The first overload is mostly used. The second and third overloads are similar to the first one except that they also accept one or more referenced models.
The first parameter
readers
takes a set ofXmlReader
each of which reads a CSDL document. The second paramtermodel
passes out the parsed model. The third parametererrors
passes out the errors when parsing the CSDL document. If the return value of this method istrue
(indicating parse success), theerrors
should be an empty otherwise it will contain all the model errors.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 both the csdl.xml file and the csdl1.xml file 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 the previous sections. EdmLib APIs support adding navigation properties targetting some entity set in the entity container as well as contained entity set belonging to some specific navigation property.This section shows how to define navigation properties using EdmLib APIs. We will continue to use and extend the sample from the previous sections. 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 and targets one or moreCustomer
entites in the entity setCustomers
; - Sets the
TargetMultiplicity
property toEdmMultiplicity.Many
indicating that one customer can have many orders. Other possible values areZeroOrOne
andOne
;
Add an Entity Type Order and an 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
property targetting one or more settled orders in the entity setOrders
; - Adds a
Intentions
property targetting a contained entity set of unsettled orders that should not be listed in the entity setOrders
.
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:
References
- Adds a navigation property
-
2.4 Define singleton
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 continue to use and extend the sample from the previous sections. 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 csdl.xml file under the output directory. The content of csdl.xml should look like the following:
References
[Tutorial & Sample] Use Singleton to define your special entity.
-
2.5 Define type inheritance
Type inheritance means defining derived types. EdmLib supports defining both derived entity types and derived complex types. Adding a derived entity (complex) type is almost the same as adding an normal entity (complex) except that an additional base type needs to be provided.This section shows how to define entity (complex) type inheritance using EdmLib APIs. We will continue to use and extend the sample from the previous sections. Add a 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 a derived entity type
UrgentOrder
within the namespaceSample.NS
whose base type isSample.NS.Order
; - Adds a structural property
Deadline
of typeEdm.Date
; - Adds the
Sample.NS.UrgentOrder
type to the entity data model.
Add a 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 a derived complex type
WorkAddress
within the namespaceSample.NS
whose base type isSample.NS.Address
; - Adds a structural property
Company
of typeEdm.String
; - Adds the
Sample.NS.WorkAddress
type to the entity data model.
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:
- Defines a derived entity type
-
2.6 Define operations
EdmLib supports defining all types of operations (actions or functions) and operation imports (action imports or function imports). Besides the conceptual difference between actions and functions, the way to define them could actually be shared among actions and functions.This section shows how to define operations and operation imports using EdmLib APIs. We will continue to use and extend the sample from the previous sections. Add a 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
with no return type; - Adds a binding parameter
customer
of typeSample.NS.Customer
; - Adds a parameter
rating
of typeEdm.Int32
; - Adds the
Sample.NS.Rate
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 composable function
MostExpensive
within the namespaceSample.NS
; - Has no parameter;
- Adds the
Sample.NS.MostExpensive
action to the model.
Add a Function Import MostValuable
In the SampleModelBuilder.cs file, add the following
using
clause: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:
- Directly adds a function import
MostValuable
into the default container; - Lets the function import return a
Sample.NS.Order
from and limited to 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 arbitrary valid name).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:
- 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
element in the schema as well as the targetted model elements (inline annotations). Users can specify the serialization location using EdmLib API.This section shows how to define annotations using EdmLib APIs. We will continue to use and extend the sample from the previous sections.Add an Annotation to the Entity Set Customers
In the SampleModelBuilder.cs file, add the following
using
clause: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
targetting the entity setCustomers
to 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
targetting the propertyCustomer.Name
.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:
-
2.8 Using model utilities
The model utilities are made up of 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 the code handling the entity data models. These methods can be roughly classified into five categories: - Searching. The naming convention is
Find<ElementName>
(e.g.,IEdmModel.FindDeclaredType()
); - 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<T>
); - Setter. The naming convention is
Set<Name>
(e.g.,IEdmModel.SetEdmVersion
).
The mostly used parts are Searching, Predicate and Information. The extension methods of the latter two parts are trivial because they work literally as their names imply. So this section will mainly cover Searching. We will continue to use and extend the sample from the previous sections.
Add the Sample Code
In the Program.cs file, 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 of 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 some types defined in another model, you can reference that model in your own model. Typically when we talking about model referencing, we have a main model and one or more sub models. The main model references the sub models. But that is not an absolute role because a main model can also be referenced by another model. That is to say models can have mutual references.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 or by EDMX (CSDL). If you would like to create the model by writing code, you can take a look at the first subsection. If you want to create your model by reading an EDMX file, please refer to the second subsection. Define Model References by Code
Let us begin by defining the first sub model
subModel1
. The model contains a complex typeNS1.Complex1
which will have a structural property of another complex type defined in another model. We also add an EDM reference tosubModel1
pointing to the second model located athttp://model2
. This 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 is that the property type is defined in the other model.After defining the two sub models, we now define the main model. This model 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 EDMX
As an example, we store the EDMX 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 models constructed in either way should be the same. For the complete sample code in this section, please visit
https://github.com/OData/ODataSamples/blob/master/Components/Edm/Program.cs#L122-L228
. -
2.10 Other topics
References
[Tutorial & Sample] How to Use Open Type in OData. [Tutorial & Sample] Using Unsigned Integers in OData.
-
2.11 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
ofEdm.String
.Create an entity type
Test.Order
with a composite key consisting of two key propertiescustomerId
andorderId
both ofEdm.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
ofEdm.String
,orderId
ofEdm.String
andid
ofEdm.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 same order.Then you can query the
details
either by a full key predicatehttp://host/customers('customerId')/orders(customerId='customerId',orderId='orderId')/Test.DetailedOrder/details(customerId='customerId',orderId='orderId',id=1)
or a 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
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
clause: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
ODataComplexValue
for theAddress
type could be constructed as follows:Construct More Complex Spatial Intances
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.TDDUnitTests
project. Please find the source code here.References
4. CLIENT
-
Basic CRUD Operations
Request an entity set
The
Execute()
API call will return anIEnumerable<Person>
.Request an individual entity
Either use the
Where()
API call:or use the
ByKey()
API:or
The
person
object returned are all of the typePerson
.Update an entity
Please be noted that the request is not sent until you call the
SaveChanges()
API. Thecontext
will track all the changes you make to the entities attached to it (by gettingperson
from the service you attached it to thecontext
) and will send requests for the changes whenSaveChanges
is called.The sample above will send a
PATCH
request to the service in which the body is the wholePerson
containing properties that are unchanged. There is also a way to track the changes on the property level to only send changed properties in an update request. It will be introduced in later posts.Delete an entity
-
Basic Queries Options
$filter
For
GET http://host/service/EntitySet?$filter=Prop eq value
:For
GET http://host/service/EntitySet?$filter=endswith(Prop, value)
:For
GET http://host/service/EntitySet?$filter=PropCol/$count eq value
:For
GET http://host/service/EntitySet?$filter=PropCol/any(d:d/Prop gt value)
:$count
For
GET http://host/service/EntitySet/$count
:For
GET http://host/service/EntitySet?$count=true
:$orderby
For
GET http://host/service/EntitySet?$orderby=Prop
:For
GET http://host/service/EntitySet?$orderby=Prop desc
:For
GET http://host/service/EntitySet?$orderby=PropCol/$count
:$skip
$top
$expand
$select
A simple combined query combined
The order of the query options matters.
-
Deal with server-driven paging
The OData Client for .NET deals with server-driven paging with the help of DataServiceQueryContinuation
andDataServiceQueryContinuation<T>
. They are classes that contain the next link of the partial set of items.Example: -
Get Response Content for Data Modification Requests
When the service doesn’t respond with 204 No Content
to data modification requests, the response contains a non-empty body. The code below helps to retrieve the body content: -
Client Annotation Support
Background
Before ODataLib 6.10.0, OData core lib has supported metadata annotations for metadata element in model and instance annotations for a particular instance in payload. But on client side, there isn’t a good way to get these annotations. So In ODataLib 6.10.0, we provided several APIs to enable user to get annotations on client side. Basically, OData client follows the rules defined in OData V4.0 protocol (see 6.4 Vocabulary Extensibility) to get instance annotations or metadata annotations.
How to get annotations on client side
All client CLR types in this tutorial are generated by OData Client Code Generator. Before we dive into this tutorial, you can read “How to use OData Client Code Generator to generate client-side proxy class” for generating client side proxy class. OData Client provided following APIs for getting annotations in
DataServiceContext
class.OData Client provided following APIs for getting annotations in
DataServiceContext
class.public bool TryGetAnnotation<TResult>(object source, string term, string qualifier, out TResult annotation) public bool TryGetAnnotation<TResult>(object source, string term, out TResult annotation) public bool TryGetAnnotation<TFunc, TResult>(Expression<TFunc> expression, string term, string qualifier, out TResult annotation) public bool TryGetAnnotation<TFunc, TResult>(Expression<TFunc> expression, string term, out TResult annotation)
The first two APIs are for getting annotations associated with a specified object. The last two APIs are for getting annotations for a property, a navigation property, an entity set, a singleton, an operation or an operation import.
In these APIs, term is the full qualified name of term, qualifier should be provided if an annotation contains qualifier, which means, if the annotation defines qualifier, but user use null as qualifier, then these APIs will return false.
In following part, we will give some examples for what we have supported in ODataLib 6.10.0. But for other elements that we didn’t mentioned, they haven’t been supported yet.
Request preference odata.include-annotations
To get instance annotations, we need to set odata.include-annotations preference in request to specify the set of annotations the client requests to be included.
public static void Main(string[] args) { DefaultContainer dsc = new DefaultContainer(new Uri("http://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/")); dsc.SendingRequest2 += (sender, eventArgs) => { eventArgs.RequestMessage.SetHeader("Prefer", "odata.include-annotations="*""); }; }
Please refer to 8.2.8.4 Preference odata.include-annotations for the rules of this preference.
Get annotations Code Sample
Get an instance annotation for a feed
var personQueryResponse = dsc.People.Execute(); personQueryResponse.ToList(); dsc.TryGetAnnotation(personQueryResponse, fullQualifiedTermName, null /*qualifier*/, out annotation);
Please note the first parameter should be an
QueryOperationResponse<Person>
.For feed, we have to enumerate the response to materialize the annotation. So in this code block, we call
personQueryResponse.ToList()
to enumerate the response, and then we callpublic bool TryGetAnnotation<TResult>(object source, string term, string qualifier, out TResult annotation)
to get the instance annotations for the feed.Currently, qualifier is not supported for instance annotation. So we pass in null for qualifier parameter. Also, we can use
public bool TryGetAnnotation<TResult>(object source, string term, out TResult annotation)
which doesn’t contain qualifier parameter.Please note, if you want to get a metadata annotation for an entity set, you should use the last two APIs, which will be mentioned later.
Get an annotation for an entity
var person = dsc.People.ByKey("russellwhyte").GetValue(); bool result = dsc.TryGetAnnotation(person, fullQualifiedTermName, qualifier, out annotation);
Please note the first parameter should be the Clr object. This API will firstly try to get the instance annotation of the
fullQualifiedTermName
andqualifier
. If the instance annotation doesn’t exist, it will try to get the metadata annotation of thefullQualifiedTermName
andqualifier
.Get an annotation for a property in an entity or a navigation property
var person = dsc.People.ByKey("russellwhyte").GetValue(); // Try to get an annotation for a property dsc.TryGetAnnotation<Func<ObservableCollection<string>>, string>(() => person.Emails, fullQualifiedTermName, qualifier, out annotation); // Try to get an annotation for a navigation property dsc.TryGetAnnotation<Func<Photo>, string>(() => person.Photo, fullQualifiedTermName, qualifier, out annotation);
The first parameter is the closure lambda expression which is to access the property. The API will firstly try to get the instance annotation, if it doesn’t exist, it will try to get the metadata annotation for the property.
Get annotation for a complex value
var address = dsc.People.ByKey("russellwhyte").Select(p => p.AddressInfo).GetValue(); dsc.TryGetAnnotation(address, fullQualifiedTermName, qualifier, out annotation);
The first parameter is the Clr instance of a complex type. This API will firstly try to get the instance annotation of this complex value, if it doesn’t exit, it will try to get the metadata annotation for the complex type of the instance.
Get metadata annotation for an entity set/singleton/function/function import/action/action import
In section “Get an instance annotation for a feed”, we know that to get metadata annotation for an entity set, we should use the last two APIs. This rule is also apply to singleton, function, function import, action and action import.
// Try to get a metadata annotation for an entity set dsc.TryGetAnnotation<Func<DataServiceQuery<Person>>, string>(() => dsc.People, fullQualifiedTermName, qualifier, out annotation); // Try to get a metadata annotation for a singleton dsc.TryGetAnnotation<Func<PersonSingle>, string>(()=>dsc.Me, fullQualifiedTermName, qualifier, out annotation); // Try to get a metadata annotation for a function bound to a person var person = dsc.People.ByKey("russellwhyte").GetValue(); dsc.TryGetAnnotation<Func<AirlineSingle>, string>(()=>person.GetFavoriteAirline(), fullQualifiedTermName, qualifier, out annotation); // Try to get a metadata annotaiton for an action bound to a person dsc.TryGetAnnotation<Func<string, int, DataServiceActionQuery>, string>((userName, tripId) => person.ShareTrip(userName, tripId), fullQualifiedTermName, qualifier, out annotation); // Try to get a metadata annotation for a function import dsc.TryGetAnnotation<Func<double, double, AirportSingle>, string>((lat, lon) => dsc.GetNearestAirport(lat, lon), fullQualifiedTermName, qualifier, out annotation); // Try to get a metadata annotation for an action import dsc.TryGetAnnotation<Func<DataServiceActionQuery>, string>(() => dsc.ResetDataSource(), fullQualifiedTermName, qualifier, out annotation);
-
Client Hooks in OData Client
OData Client provides several ways to allow developers to hook into the client request and response. It gives developers the opportunity to inspect, adjust or replace some request or response.This doc will give you several real world examples to explain all these kinds of methods in OData Client. Event Handler
DataServiceContext
provided three events to let developers to hook up to.BuildingRequest
public event EventHandler<BuildingRequestEventArgs> BuildingRequest;
This event is fired before a request message object is built, giving the handler the opportunity to inspect, adjust and/or replace some request information before the message is built. This event is always used to modify the outgoing Url of the request, alter request headers or change the http method.
DefaultContainer dataServiceContext = new DefaultContainer(new Uri("http://services.odata.org/V4/TripPinServiceRW/")); dataServiceContext.BuildingRequest += (sender, eventArgs)=> { eventArgs.RequestUri = new Uri("http://services.odata.org/V4/(S(ghojd5jj5d33cwotkyfwn431))/TripPinServiceRW/People"); }; dataServiceContext.People.Execute();
Developers can also change the HttpMethod of the request.
dataServiceContext.BuildingRequest += (sender, eventArgs) => { eventArgs.Method = "PUT"; };
ReceivingResponse
public event EventHandler<ReceivingResponseEventArgs> ReceivingResponse;
This event is fired when a response is received by the client. It is fired for both top level responses and each operation or query within a batch response.
For a non-batch response:
DefaultContainer dataServiceContext = new DefaultContainer(new Uri("http://services.odata.org/V4/TripPinServiceRW/")); dataServiceContext.ReceivingResponse += (sender, eventArgs) => { Console.WriteLine(eventArgs.ResponseMessage.GetHeader("OData-Version")); }; dataServiceContext.People.First();
For a batch request for query, the
ReceivingResponse
will firstly be fired when the client receives the top level response. Then, the event will be fired when the client enumerates the innerQueryOperationResponse
.ReceivingResponse
is fired as many times as the responses are enumerated. So about the following code, before the client executesforeach
, the code will only print theContent-Type
for the top-level request. The last several lines of following code enumerates each of theQueryOperationResponse
.RecivingResponse
will be fired accordingly.DefaultContainer dataServiceContext = new DefaultContainer(new Uri("http://services.odata.org/V4/(S(irl1k2jt4e4bscxuk30bpgji))/TripPinServiceRW/")); dataServiceContext.ReceivingResponse += (sender, eventArgs) => { Console.WriteLine(eventArgs.ResponseMessage.GetHeader("Content-Type")); }; var responses = dataServiceContext.ExecuteBatch(dataServiceContext.People, dataServiceContext.Airlines); // Enumerate the response will fire the ReceivingResponse for each of the inner query foreach (QueryOperationResponse response in responses) { }
But for a batch request for changes.
ReceivingResponse
will be fired for both top level response and inner response even the client doesn’t enumerate the response. So the following code will print200 204 204
200 is the response status code of the top level message. the other two 204 status codes are of the inner responses.
DefaultContainer dataServiceContext = new DefaultContainer(new Uri("http://services.odata.org/V4/(S(irl1k2jt4e4bscxuk30bpgji))/TripPinServiceRW/")); var p1 = dataServiceContext.People.First(); var p2 = dataServiceContext.People.Skip(1).First(); dataServiceContext.ReceivingResponse += (sender, eventArgs) => { Console.WriteLine(eventArgs.ResponseMessage.StatusCode); }; p1.FirstName = "aa"; p2.FirstName = "bb"; dataServiceContext.UpdateObject(p1); dataServiceContext.UpdateObject(p2); dataServiceContext.SaveChanges(Microsoft.OData.Client.SaveChangesOptions.BatchWithSingleChangeset);
SendingRequest2
public event EventHandler<SendingRequest2EventArgs> SendingRequest2;
This event is fired before a request is sent to the server, giving the handler the opportunity to inspect, adjust and/or replace the WebRequest object used to perform the request.
The most common use of this event is to set the headers of the request. You can set the header for response payload format, or the authentication information like token or cert name. You also can use this event to set preferences, If-Match headers.
The code below will add the
odata.include-annotations
preference in request header to enable getting instance annotations.DefaultContainer dataServiceContext = new DefaultContainer(new Uri("http://services.odata.org/V4/(S(ghojd5jj5d33cwotkyfwn431))/TripPinServiceRW/")); dataServiceContext.SendingRequest2 += (sender, eventArgs) => { eventArgs.RequestMessage.SetHeader("Prefer", "odata.include-annotations="*""); }; dataServiceContext.People.Execute();
You can also use this event to check other information in the request message.
DataServiceClientConfigurations
DataServiceContext
defines aConfigurations
property ofDataServiceClientConfigurations
which contains aRequestPipeline
andResponsePipeline
. These two pipelines provide several hooks to developers to hook into the client request or response.OnMessageCreating
OnMessageCreating
is a property of theRequestPipeline
.public Func<DataServiceClientRequestMessageArgs, DataServiceClientRequestMessage> OnMessageCreating
Developers can use this function to customize the request message.
Customize request message
Following code provides a sample which overrides the
GetResponse()
method in user-defined request message which fakes a response message. We define a client request message which inheritsHttpWebRequestMessage
.HttpWebRequestMessage
is a sub class ofDataServiceClientRequestMessage
public class CustomizedRequestMessage : HttpWebRequestMessage { public string Response { get; set; } public Dictionary<string, string> CustomizedHeaders { get; set; } public CustomizedRequestMessage(DataServiceClientRequestMessageArgs args) : base(args) { } public CustomizedRequestMessage(DataServiceClientRequestMessageArgs args, string response, Dictionary<string, string> headers) : base(args) { this.Response = response; this.CustomizedHeaders = headers; } public override IODataResponseMessage GetResponse() { return new HttpWebResponseMessage( this.CustomizedHeaders, 200, () => { byte[] byteArray = Encoding.UTF8.GetBytes(this.Response); return new MemoryStream(byteArray); }); } }
Set
OnMessageCreating
Then, Developers can replace the default client message with
CustomizedClientRequestMessage
by using following code. Then if the client sends a request after this setting, it will automatically return the fake response message.DefaultContainer dataServiceContext = new DefaultContainer(new Uri("http://services.odata.org/V4/(S(irl1k2jt4e4bscxuk30bpgji))/TripPinServiceRW/")); string response = "..." //set the response dataServiceContext.Configurations.RequestPipeline.OnMessageCreating = (args) => { return new CustomizedRequestMessage( args, response, new Dictionary<string, string>() { {"Content-Type", "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8"}, {"Preference-Applied", "odata.include-annotations="*""} }); }; dataServiceContext.PeoplePlus.ByKey("Jason").GetValue();
OnEntryStarting
OnEntryStarting
is a method of theRequestPipeline
.public DataServiceClientRequestPipelineConfiguration OnEntryStarting(Action<WritingEntryArgs> action)
Developer can use this function to control the information of an
ODataEntry
to be serialized.Modify ODataEntry properties
Following code provides a sample to add properties to an
ODataEntry
.public static void AddProperties(this ODataEntry entry, params ODataProperty[] newProperties) { var odataProps = entry.Properties as List<ODataProperty>; if (odataProps == null) { odataProps = new List<ODataProperty>(entry.Properties); } odataProps.AddRange(newProperties); entry.Properties = odataProps; }
Set
OnEntryStarting
Then, to add new properties in the
OdataEntry
, developers can callAddProperties
inOnEntryStarting
.DefaultContainer dataServiceContext = new DefaultContainer(new Uri("http://services.odata.org/v4/(S(ghojd5jj5d33cwotkyfwn431))/TripPinServiceRW/")); dataServiceContext.Configurations.RequestPipeline.OnEntryStarting( arg => { arg.Entry.AddProperties(new ODataProperty { Name = "NewProperty", Value = "new property" }); }); var person = dataServiceContext.People.ByKey("russellwhyte").GetValue(); dataServiceContext.UpdateObject(person); dataServiceContext.SaveChanges();
More client hooks in RequestPipeline && ResponsePipeline
These two configurations provide more other client hooks in request pipeline and response pipeline.
Please refer to this link for details.
-
Asynchronous operations
All samples in this doc are based on the Trippin Service. You can follow “How to use OData Client Code Generator to generate client-side proxy class” to generate the client proxy file.OData Client for .NET provides a serial of Begin/End methods to support asynchronous operations, such as executing queries and saving changes. Each Begin method takes a state parameter that can pass a state object to the callback. This state object is retrieved from the IAsyncResult
that is supplied with the callback and is used to call the corresponding End method to complete the asynchronous operation.OData Client for .NET (from 6.4.0) also provides another set of asynchronous APIs in .NET 4.0 format, likeExecuteAsync
;#Asynchronous Query #Query an Entity Set
DataServiceQuery<TElement>
providesBeginExecute
andEndExecute
methods to support query a collection of entitiesDefaultContainer dsc = new DefaultContainer(new Uri("http://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/")); public void AsyncQueryAnEntitySet() { var people = dsc.People; people.BeginExecute(ReadingPeople, people); //Waiting for the Begin/End finished. System.Threading.Thread.Sleep(5000); } public void ReadingPeople(IAsyncResult ar) { var peopleQuery = ar.AsyncState as DataServiceQuery<Person>; if (peopleQuery != null) { var people = peopleQuery.EndExecute(ar); if (people != null) { foreach (var p in people) { Console.WriteLine(p.UserName); } } } }
The
EndExecute
API returns anIEnumerable<Person>
.You also can use
DataServiceQuery<TElement>.ExecuteAsync
to query an entity set.public async Task AsyncAPIGetEntitySet() { var response = await dsc.People.ExecuteAsync(); foreach (var p in (response as QueryOperationResponse<Person>)) { Console.WriteLine(p.UserName); } }
Query an Entity Set with Paging
DataServiceContext
providesBeginExecute
method which could take aDataServiceQueryContinuation<TElement>
parameter to get the next page of data in a paged query result.public void AsyncQueryAnEntitySetWithPaging() { var people = dsc.People; people.BeginExecute(ReadingPeople, people); //Waiting for the Begin/End finished. System.Threading.Thread.Sleep(10000); } public void ReadingPeople(IAsyncResult ar) { var peopleQuery = ar.AsyncState as DataServiceQuery<Person>; if (peopleQuery != null) { var response = peopleQuery.EndExecute(ar) as QueryOperationResponse<Person>; if (response != null) { foreach (var p in response) { Console.WriteLine(p.UserName); } } var continuation = response.GetContinuation(); if (continuation != null) { dsc.BeginExecute(continuation, ReadingContinuation, dsc); } } } public void ReadingContinuation(IAsyncResult ar) { var dsc = ar.AsyncState as DataServiceContext; if (dsc != null) { var response = dsc.EndExecute<Person>(ar) as QueryOperationResponse<Person>; if (response != null) { foreach (var p in response) { Console.WriteLine(p.UserName); } } var continuation = response.GetContinuation(); if (continuation != null) { dsc.BeginExecute(continuation, ReadingContinuation, dsc); } } }
You also can use
DataServiceContext.ExecuteAsync
to get the next page of an entity set.public async Task AsyncAPIPaging() { var response = (await dsc.People.ExecuteAsync()) as QueryOperationResponse<Person>; foreach (var p in response) { Console.WriteLine(p.UserName); } var continuation = response.GetContinuation(); while (continuation != null) { response = (await dsc.ExecuteAsync(continuation)) as QueryOperationResponse<Person>; foreach (var p in response) { Console.WriteLine(p.UserName); } continuation = response.GetContinuation(); } }
Query a Single Entity
DataServiceContext
providesBeginGetValue
andEndGetValue
methods to support querying a single entitypublic void AsyncQueryAnEntitySet() { var person = dsc.People.ByKey("russellwhyte"); person.BeginGetValue(ReadingPerson, person); //Waiting for the Begin/End finished. System.Threading.Thread.Sleep(5000); } public void ReadingPerson(IAsyncResult ar) { var personQuery = ar.AsyncState as DataServiceQuerySingle<Person>; if (personQuery != null) { var person = personQuery.EndGetValue(ar); Console.WriteLine(person.UserName); } }
Or, you can use
DataServiceContext.GetValueAsync
to support such query.public async Task AsyncAPIGetSingleEntity() { var russell = await dsc.People.ByKey("russellwhyte").GetValueAsync(); Console.WriteLine(russell.UserName); }
Query Navigation property
Expand
method ofDataServiceQuery<TElement>
provides a way to query related entities. But if you want to query the navigation property separately,DataServiceContext
provides LoadProperty method to support it.BeginLoadProperty
andEndLoadProperty
methods are the related asynchronous APIs.public void AsyncQueryNavigationProperty() { var me = dsc.Me.GetValue(); dsc.BeginLoadProperty(me, "Trips", ReadingTrips, dsc); //Waiting for the Begin/End finished. System.Threading.Thread.Sleep(10000); } public void ReadingTrips(IAsyncResult ar) { var dsc = ar.AsyncState as DataServiceContext; if (dsc != null) { var response = dsc.EndLoadProperty(ar); if (response != null) { foreach (Trip t in response) { Console.WriteLine(t.Name); } } } }
You can also use
DataServiceContext.LoadPropertyAsync
to query the related properties.public async Task AsyncAPIGetNavigation() { var me = await dsc.Me.GetValueAsync(); await dsc.LoadPropertyAsync(me, "Trips"); foreach(var t in me.Trips) { Console.WriteLine(t.Name); } }
Query a Batch
DataServiceContext
providesBeginExecuteBatch
to put several query in a batch. The queries are specified asDataServiceRequest<TElement>
instances. TheEndExecuteBatch
returns aDataServiceResponse
that represents the response of the batch request as a whole. Individual query responses are represented asDataServiceResponse
objects that can be accessed by enumerating theDataServiceResponse
instance.public void AsyncQueryBatch() { var requests = new DataServiceRequest[] { dsc.People, dsc.Airlines }; dsc.BeginExecuteBatch(ReadingBatch, dsc, requests); System.Threading.Thread.Sleep(5000); } public void ReadingBatch(IAsyncResult ar) { var dsc = ar.AsyncState as DataServiceContext; var response = dsc.EndExecuteBatch(ar); foreach (var r in response) { var people = r as QueryOperationResponse<Person>; if (people != null) { foreach (Person p in people) { Console.WriteLine(p.UserName); } } var airlines = r as QueryOperationResponse<Airline>; if (airlines != null) { foreach (var airline in airlines) { Console.WriteLine(airline.Name); } } } }
Or, you can use
ExecuteBatchAsync
to do the same thing.public async Task AsyncAPIExecuteBatch() { var requests = new DataServiceRequest[] { dsc.People, dsc.Airlines }; var response = await dsc.ExecuteBatchAsync(requests); foreach (var r in response) { var people = r as QueryOperationResponse<Person>; if (people != null) { foreach (Person p in people) { Console.WriteLine(p.UserName); } } var airlines = r as QueryOperationResponse<Airline>; if (airlines != null) { foreach (var airline in airlines) { Console.WriteLine(airline.Name); } } } }
Create/Update/Delete an Entity or a relationship
DataServiceContext
providesBeginSaveChanges
andEndSavechanges
methods to asynchronously submits the pending changes to the data service. Changes are added to theDataServiceContext
by calling AddObject, UpdateObject, DeleteObject, AddLink, DeleteLink SetLink, SetSaveStream, etc.You can use the
SaveChangesOption
to control whether you need to send a batch request.##Create an entity##
DefaultContainer dsc = new DefaultContainer(new Uri("http://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/")); public void AsyncCreatePerson() { var person = new Person() { FirstName = "Tom", LastName = "White", UserName = "TomWhite", }; dsc.AddToPeople(person); dsc.BeginSaveChanges(CreatingPerson, dsc); //Waiting for the Begin/End finished. System.Threading.Thread.Sleep(10000); } public void CreatingPerson(IAsyncResult ar) { var dsc = ar.AsyncState as DataServiceContext; if (dsc != null) { var response = dsc.EndSaveChanges(ar); } }
Update an entity
public void AsyncUpdatePerson() { // Get the single entity first. var personQuery = dsc.People.ByKey("TomWhite"); var ar = personQuery.BeginGetValue(null, null); ar.AsyncWaitHandle.WaitOne(); var person = personQuery.EndGetValue(ar); person.LastName = "Bourne"; dsc.UpdateObject(person); dsc.BeginSaveChanges(ChangingPerson, dsc); //Waiting for the Begin/End finished. System.Threading.Thread.Sleep(10000); }
Delete an entity
The code is almost the same with that in Update an entity part, you only need to change the update part to
dsc.DeleteObject(person);
Modify Link
DataServiceContext.BeginSaveChanges
can submit the pending changes of relationships modification to service. The code is almost same as before.Use Async APIs to modify a entity
DataServiceContext
providesSaveChangesAsync
to support all the modification operation.public async Task AsyncModifyEntity() { var person = new Person() { FirstName = "Alica", LastName = "White", UserName = "TomWhite", }; dsc.AddToPeople(person); await dsc.SaveChangesAsync(); person.FirstName = "Tom"; dsc.UpdateObject(person); await dsc.SaveChangesAsync(); dsc.DeleteObject(person); await dsc.SaveChangesAsync(); }
#Read Stream#
DataServiceContext
providesBeginGetReadStream
andEndGetReadStream
to support asynchronously requesting the binary data stream that belongs to the requested entity.public void ReadingStream(IAsyncResult ar) { var dsc = ar.AsyncState as DataServiceContext; var receiveStream = dsc.EndGetReadStream(ar).Stream; var sr = new StreamReader(receiveStream).ReadToEnd(); Console.WriteLine(sr.Length); } public void AsyncAPIGetReadStream() { var task = dsc.Photos.ByKey(1).GetValueAsync(); task.Wait(); dsc.GetReadStreamAsync(task.Result, new DataServiceRequestArgs()); }
You also can use
GetReadStreamAsync
to get the binary data.public async Task AsyncAPIGetReadStream() { var task = dsc.Photos.ByKey(1).GetValueAsync(); task.Wait(); var stream = (await dsc.GetReadStreamAsync(task.Result, new DataServiceRequestArgs())).Stream; var sr = new StreamReader(stream).ReadToEnd(); Console.WriteLine(sr.Length); }
-
Batch Operations
OData Client for .NET supports batch processing of requests to an OData service. This ensures that all operations in the batch are sent to the data service in a single HTTP request, enables the server to process the operations atomically, and reduces the number of round trips to the service.OData Client for .NET doesn’t support sending both query and change in one batch request. Batch Query
To execute multiple queries in a single batch, you must create each query in the batch as a separate instance of the
DataServiceRequest<TElement>
class. The batched query requests are sent to the data service when theExecuteBatch
method is called. It contains the query request objects.This method accepts an array of
DataServiceRequest
as parameters. It returns aDataServiceResponse
object, which is a collection ofQueryOperationResponse<T>
objects that represent responses to individual queries in the batch, each of which contains either a collection of objects returned by the query or error information. When any single query operation in the batch fails, error information is returned in theQueryOperationResponse<T>
object for the operation that failed and the remaining operations are still executed.ExecuteBatch
will send a “POST” request tohttp://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/$batch
. Each internal request contains its own http method “GET”.The payload of the request is as following:
--batch_d3bcb804-ee77-4921-9a45-761f98d32029 Content-Type: application/http Content-Transfer-Encoding: binary GET http://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/People HTTP/1.1 OData-Version: 4.0 OData-MaxVersion: 4.0 Accept: application/json;odata.metadata=minimal Accept-Charset: UTF-8 User-Agent: Microsoft ADO.NET Data Services --batch_d3bcb804-ee77-4921-9a45-761f98d32029 Content-Type: application/http Content-Transfer-Encoding: binary GET http://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/Airlines HTTP/1.1 OData-Version: 4.0 OData-MaxVersion: 4.0 Accept: application/json;odata.metadata=minimal Accept-Charset: UTF-8 User-Agent: Microsoft ADO.NET Data Services --batch_d3bcb804-ee77-4921-9a45-761f98d32029--
Batch Modification
In order to batch a set of changes to the server,
ODataServiceContext
providesSaveChangesOptions.BatchWithSingleChangeset
andSaveChangesOptions.BatchWithIndependentOperations
whenSaveChanges
.SaveChangesOptions.BatchWithSingleChangeset
will save changes in a single change set in a batch request.SaveChangesOptions.BatchWithIndependentOperations
will save each change independently in a batch request.You can refer to odata v4.0 protocol 11.7 to get more details about batch request and whether requests should be contained in one change set or not.
The payload for all requests in one change set is like following
This will send request with URL http://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/$batch.
The request headers contain following two headers:
Content-Type: multipart/mixed; boundary=batch_06d8a02a-854a-4a21-8e5c-f737bbd2dea8 Accept: multipart/mixed
The request Payload is as following:
--batch_06d8a02a-854a-4a21-8e5c-f737bbd2dea8 Content-Type: multipart/mixed; boundary=changeset_b98a784d-af07-4723-9d5c-4722801f4c4d --changeset_b98a784d-af07-4723-9d5c-4722801f4c4d Content-Type: application/http Content-Transfer-Encoding: binary Content-ID: 3 PATCH http://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/Me HTTP/1.1 OData-Version: 4.0 OData-MaxVersion: 4.0 Content-Type: application/json;odata.metadata=minimal If-Match: W/"08D24EFA2E435C91" Accept: application/json;odata.metadata=minimal Accept-Charset: UTF-8 User-Agent: Microsoft ADO.NET Data Services {"@odata.type":"#Microsoft.OData.SampleService.Models.TripPin.Person","[email protected]":"#Collection(Microsoft.OData.SampleService.Models.TripPin.Location)","AddressInfo":[{"@odata.type":"#Microsoft.OData.SampleService.Models.TripPin.Location","Address":"P.O. Box 555","City":{"@odata.type":"#Microsoft.OData.SampleService.Models.TripPin.City","CountryRegion":"United States","Name":"Lander","Region":"WY"}}],"Concurrency":635657333837618321,"[email protected]":"#Collection(String)","Emails":["[email protected]","[email protected]"],"FirstName":"April","[email protected]":"#Microsoft.OData.SampleService.Models.TripPin.PersonGender","Gender":"Female","LastName":"Test","UserName":"aprilcline"} --changeset_b98a784d-af07-4723-9d5c-4722801f4c4d Content-Type: application/http Content-Transfer-Encoding: binary Content-ID: 4 PATCH http://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/Me/Trips(1001) HTTP/1.1 OData-Version: 4.0 OData-MaxVersion: 4.0 Content-Type: application/json;odata.metadata=minimal Accept: application/json;odata.metadata=minimal Accept-Charset: UTF-8 User-Agent: Microsoft ADO.NET Data Services {"@odata.type":"#Microsoft.OData.SampleService.Models.TripPin.Trip","Budget":3000,"Description":"Updated Trip","EndsAt":"2014-01-04T00:00:00Z","Name":"Trip in US","ShareId":"9d9b2fa0-efbf-490e-a5e3-bac8f7d47354","StartsAt":"2014-01-01T00:00:00Z","[email protected]":"#Collection(String)","Tags":["Trip in New York","business","sightseeing"],"TripId":1001} --changeset_b98a784d-af07-4723-9d5c-4722801f4c4d-- --batch_06d8a02a-854a-4a21-8e5c-f737bbd2dea8--
-
Client Tracking
OData Client for .NET supports two levels tracking : entity tracking and property tracking(only top level properties). Entity tracking enables you to track an entity in DataServiceContext
. You can enable property tracking by aid ofDataServiceCollectionOfT
.Entity Tracking
DataServiceContext
provides several ways to track an entity.- Newly added entities will be automatically tracked.
- If you use
DataServiceContext.AttachTo
to attach an entity,DataServiceContext
will track the entity. - Entities returned by queries are also tracked if
DataServiceContext.MergeOption
is notMergeOption.NoTracking
.
Once entities are tracked, you can use
DataServiceContext.EntityTracker
to get each entity descriptor which is used to describe the entity on client side. the entity tracker can also be used to get the link descriptor of all tracked links.Once entities are tracked, the changes of these entities can be sent back to the data service when you call
DataServiceContext.SaveChanges
method.If you are using
MergeOption.NoTracking
when you query an entity. You cannot get ETag of the entity fromDataServiceContext
if it exists, since you cannot get the entity descriptor for the entity. Then, if you want to call AttachTo to track the entity, you need provide the ETag of the entity.DataServiceContext
tracks each relationship as a link. You can use methodsAddRelatedObject
,AttachLink
,AddLink
,SetLink
,DetachLink
,DeleteLink
to track a link.One sample to use
AttachTo
andDeleteLink
.Property Tracking
Please refer to client property tracking for patch for detail.
-
Use HttpClient in OData Client
In this session, we will dive into how to use HttpClient in OData client request. We will use the hook mechanism in OData client which has been introduced in Client Hooks in OData Client.OData client enables developers to customize request message, and use it in DataServiceContext.Configurations.RequestPipeline.OnMessageCreating
. This function will be triggered when creating request message. It will return anIODataRequestMessage
.Following is the code how to useOnMessageCreating
.In this sample, we create a
HttpClientRequestMessage
instance inOnMessageCreating
method.HttpClientRequestMessage
is a class derived fromDataServiceClientRequestMessage
. In this class, we use MemoryStream to write data, and use HttpClient to get response. Once we get the HttpResponseMessage, we will convert it toIODataResponseMessage
. So we also write aHttpClientResponseMessage
class which implementsIODataResponseMessage
.
5. RELEASE NOTES
-
(V3) ODataLib 5.6.4
New Features:
[GitHub issue #144] ODataLib now suppresses the errors in reading open entity’s undeclared collection or complex property value
Bug Fixes:
[GitHub issue #60] Fix an issue that $select does not work with EntityFramework 5
-
(V4) ODataLib 6.11.0
New Features:
[GitHub issue #23] ODataLib now supports parsing URI path template.
[GitHub issue #71] EdmLib now supports adding vocabulary annotations to EdmEnumMember.
[GitHub issue #80] OData client for .NET now supports abstract entity type without key.
[GitHub issue #85] ODataLib now supports additional preference headers: odata.track-changes, odata.maxpagesize and odata.continue-on-error.
[GitHub issue #87] ODataLib now supports setting filter query option in ExpandedNavigationSelectItem.
[GitHub issue #94] ODataLib now supports $levels in ODataUriBuilder.
[Github issue #144] ODataLib now suppresses the errors in reading open entity’s undeclared primitive, collection and complex property value.
Improvements:
[GitHub issue #101] Improve the performance of DataServiceContext.SaveChanges when the entities are tracked by a DataServiceCollection.
Bug Fixes:
[GitHub issue #93] Fix a bug that DataServiceContext.CreateFunctionQuery should set isComposable property of DataServiceOrderedQuery.
[GitHub issue #95] Fix a bug that OData client for .NET does not support composing a query operation onto a composable function.
-
(V4) ODataLib 6.12.0
New Features:
[GitHub Issue #86] [Pull request #141 & #142 by Jeff Wight] ODataUriParser now supports parsing $skiptoken & $deltatoken query options in ODataUri.
[GitHub Issue #97] [Pull request #184 by OData team] ODataLib/EdmLib/Spatial now supports .Net Core.
[GitHub Issue #108] [Pull request #139 by Shahzor Khan] ODataLib now supports overriding primitive type payload format during serialization/deserialization.
[GitHub Issue #143] [Pull request #161 by Adam Caviness] OData client for .Net now supports adding undeclared dynamic properties to ODataEntry.
[GitHub Issue #158] [Pull request #170 by OData team] Client now supports turning on/off instance annotation materialization.
#Bug Fixes:#
[GitHub Issue #107] Fix a NullReferenceException in DataServiceCollection in OData client for .Net.
[GitHub Issue #154] Fix the bug that ODataUriParser doesn’t support passing integral literal to function parameter of type Edm.Byte, Edm.SByte and Edm.Int16.
#Improvements:# [GitHub Issue #157] [Pull request #169 by OData team] Improve the performance of OData client for .Net when client CLR type using OriginalNameAttribute.
-
(V4) ODataLib 6.13.0
New Features:
[GitHub issue #140] [Pull request #166 by chinese007] Support writing NextPageLink/Count instance annotation in top-level collection of complex type payload.
[GitHub issue #179] [Pull request #218 by gkasturi] Allow serialization of additional properties.
[GitHub issue #55] [Pull request #219 by Abhishek Kumar] Add the alternate key support.
[GitHub issue #152] [Pull request #232 by OData Team] Add the capabilities vocabulary support.
Bug Fixes:
[GitHub issue #216] [Pull request #235 by OData Team] Fix race condition in
EnumHellper.TryParseEnum
. -
(V3) ODataLib 5.7.0
New Features:
[GitHub issue #257] To support reading & writing undeclared properties in complex/entity type, OData client for .NET adds a new enum UndeclaredPropertyBehavior, and ODataLib addes ODataUndeclaredPropertyBehaviorKinds.SupportUndeclaredValueProperty.
Bug Fixes:
[GitHub issue #192] [By Soumyarupa De] Fix a global lock issue in RequestUriProcessor
[GitHub issue #243] Fix a high CPU usage issue caused by CsdlSemanticsModel class
[Github issue #149] [By maartenba] Only append parameter when it was provided in the first place
-
(V4) ODataLib 6.14.0
New Features:
[Issue #76] [PR #364] OData error with target and details.
[Issue #273] [PR #287 by Sabitha Abraham] Support navigation property without specifying navigation target.
[Issue #347] [PR #351 by Luke Dean] Support Edm.Date as Key.
[Issue #349] [PR #350 by Lee Taylor] Fix Parser conflict between UrlConventions KeyAsSegment and last segment == TypeSegment and introduce ODataSimplified convention.
[Issue #369] Support built-in annotation names without “odata.”
Bug Fixes:
[Issue #63] [PR #374 by Maxim Pashuk] Fix a bug that float numbers are not parsed with invariant culture.
[Issue #258] [PR #339] Fix an issue that core type in capabilities metadata is resolved as unresolved item.
[Issue #357] [PR #358] Fix a bug that Expanding an OData function call result throws exception.
[Issue #389] Fix a bug that OData client library memory leak causes app to run out of virtual memory.
Improvements:
[Issue #289] [PR #291 & #361] Fix a high CPU issue of CsdlSemanticsModel.
-
(V4) ODataLib 6.15.0
New Features:
[Issue #163] [PR #452] Support OData client for .NET to deserialize an entity with a non-public-setter property.
[Issue #298] [PR #446] Add support for $expand containing star(*).
[Issue #378] [PR #430 by Yogev] Support Customizing Uri Functions.
[Issue #390] [PR #433] Support reading and writing expanded navigation properties in delta payloads.
[Issue #391] [PR #448 by Yogev] Support Customizing Uri literals.
[PR #413 by kosinsky] Add basic Uri parser support for aggregations spec
[Issue #435] [PR #472 by brjohnstmsft] Support Profile 111 (portable-net45+win+wpa81).
[Issue #440] [PR #474] Support $count in $filter and $orderby.
[Issue #457] [PR #489] Support complex type cast in path segment and select item.
Bug Fixes:
[Issue #92] [PR #483 by JefWight] Add error handling for preference header with invalid values.
[Issue #336] Fix the issue that OData client for .NET cannot materialize a collection of complex type correctly.
[Issue #441] [PR #444] Fix shortened key predicate support of related entity.
[Issue #487] [PR #488 by JefWight] Provide a better exception when an unexpected PrimitiveValue node was found during JSON reader.
[PR #468 by DickvdBrink ] Fix space issues in the build script.
Improvements:
[Issue #434] Support UriParser to throw consistent and meanful exception if parsing DataTimeOffset parameter fails.
[Issue #456 & Issue #470] Cache payloadvalueconverter to improve performance.
-
(V3) ODataLib 5.8.0
New Features:
[Issue #655][PR #656] DataServiceClient – Avoid memory copy of the server response stream for synchronous requests in QueryResult.ExecuteQuery
[Issue #669][PR #670] DataServiceClient – Option to have automatic null propagation in projection.
[Issue #662][PR #663] DataServiceClient – Support lazy and un-initialized collection navigation/complex properties and single complex properties on proxy.
Bug Fixes:
[Issue #664][PR #665] DataServiceClient – Navigation property names miss escaping during SaveChanges.
[Issue #677][PR #678] DataServiceClient – OnEntityMaterialized/ReadingEntity does not fire for projected results.
[Issue #681][PR #682] DataServicesClient – DSC AutoChangeTracking does not work for subsequent server pages.
[Issue #693]DataServicesClient – Deadlock issue in multi-threads
6. ODATA FEATURES
-
Parsing URI path template
From ODataLib 6.11.0, it supports to parse Uri path template. A path template is any identifier string enclosed with curly brackets. For example: Uri templates
There are three kind of Uri template:
- Key template: ~/Customers({key})
- Function parameter template: ~/Customers/Default.MyFunction(name={name})
- Path template: ~/Customers/{dynamicProperty}
Be caution:
- please EnableUriTemplateParsing = true for UriParser.
- 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>
-
Abstract entity type support in .NET client
OData Client for .NET supports abstract entity type without key from ODataLib 6.11.0. Create model with abstract entity type
Output model
<EntityType Name="AbstractEntity" Abstract="true" /> <EntityType Name="Order" BaseType="DefaultNS.AbstractEntity"> <Key> <PropertyRef Name="OrderID" /> </Key> </EntityType>
Client generated proxy file
T4 would auto-generate code for abstract entity type like:
-
Add additional prefer header
odata.track-changes, odata.maxpagesize, odata.maxpagesize 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:
-
Composable function in Client
Composable function (function import) can have additional path segments and query options as appropriate for the returned type. Unbound composable function
For example, we have model:
GetAllProducts
is a function import and it is composable. And since actionDiscount
accepts whatGetAllProducts
returns, we can queryDiscount
afterGetAllProducts
.1. Create function query
And we can append query option to the function. For example:
The actual query would be:
2. With codegen
With OData client generator, proxy class for function and action would be auto generated. For example:
Bound composable function
Bound composable function has similiar usage, except that it is tied to a resource.
For example, we have model:
Person is the base type of Employee. Then a sample query is:
-
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
.New helper functions to set converter to model so the new converter can be applied:
Sample
Here we are trying to override the default converter to support the “R” format of date and time. It is quite simple.
1. Define DataTimeOffset converter
2. Set converter to model
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 to DateTimeOffset. -
Allow serialization of additional properties
We are now supporting to serialize addtional properties which are not advertised in Metadata from ODataLib 6.13.0. To achieve this, it is just needed to turn off full validation when creating the ODataMessageWriterSettings
.Here is a full example which is trying to write an extra propertyProp1
in the Entry. The implementation of InMemoryMessage in this sample can be found here.Then
Prop1
can be shown in the payload:{"@odata.context":"http://example.org/odata.svc/$metadata#EntitySet/$entity", "ID":102, "Name":"Bob", "Prop1":"Var1" }
-
Disable instance annotation materialization in .NET client
From 6.12.0, OData .Net client is able to disable instance annotation materialization by turning on the flag DisableInstanceAnnotationMaterialization
inDataServiceContext
.Let’s have an example to demonstrate:The response payload for the example is:{ "@odata.context":"http://localhost/$metadata#People/$entity", "PersonID":1, "FirstName":"Bob", "LastName":"Cat", "HomeAddress":{ "@odata.type":"#Microsoft.Test.OData.Services.ODataWCFService.HomeAddress", "@Microsoft.Test.OData.Services.ODataWCFService.AddressType":"Home", "Street":"1 Microsoft Way", "City":"Tokyo", "PostalCode":"98052" } }
Here we compare the effects by turning off and on the
DisableInstanceAnnotationMaterialization
flag. -
Support untyped json in ODataLib and Client
Starting from ODataV3 5.7.0, undeclared property is better supported by ODataLib and OData Client. ODataMessageReader is extended to be capabale of reading arbitrary JSON as raw string from the payload. In ODataLib
The MessageReaderSettings used to accept ODataUndeclaredPropertyBehaviorKinds.IgnoreUndeclaredValueProperty/.None as settings for reading undeclared value properties in payload, now it can accept a new setting value called ODataUndeclaredPropertyBehaviorKinds.SupportUndeclaredValueProperty. They correspond to the below behaviors:
- ODataUndeclaredPropertyBehaviorKinds.None : throws exception on undeclared property.
- ODataUndeclaredPropertyBehaviorKinds.IgnoreUndeclaredValueProperty : skip undeclared property in payload.
- ODataUndeclaredPropertyBehaviorKinds.SupportUndeclaredValueProperty : read undeclared property as either an OData valid value instance or ODataUntypedValue instance.
What the undeclared property means are:
- In open entity: a property whose name isn’t defined in the model and whose value’s type can’t be inferred (or they will be valid dynamic property in open entity if the value’s type can be determined).
- In normal entity or complex: a property whose name isn’t defined in the model.
The below messageWriterSettings will enable reading undeclared / untyped property. It reads an undeclared and untype JSON as ODataUntypedValue whose .RawJson has the raw JSON string.
In OData Client
DataServiceContext now has a new setting called UndeclaredPropertyBehavior.
- UndeclaredPropertyBehavior.None: it means respecting DataServiceContext’s old IgnoreMissingProperty boolean, for backward compatibility.
- UndeclaredPropertyBehavior.Ignore: it overwrites DataServiceContext’s old IgnoreMissingProperty boolean, means always skipping undeclared property.
- UndeclaredPropertyBehavior.Support: it overwrites DataServiceContext’s old IgnoreMissingProperty boolean, means always reading undeclared property as either an OData valid value instance or ODataUntyped instance.
The below code demostrates UndeclaredPropertyBehavior.Support:
-
Use ODataSimplified Convention In ODataUriParser
From ODataLib 6.14.0, we introduce ODataSimplified convention to make key-as-segment
anddefault
convention work side by side.Because when user use key-as-segment convention, url like/Me/Messages/Microsoft.OutlookServices.EventMessage
will always be parsed by uriParser to{Singleton}/{Navigation}/{Key}
but what customer needs is{Singleton}/{Navigation}/{Type}
. When you use ODataSimplified convention, we will try parse type first than key as a default priority to slove this problem.Turn on ODataSimplified is the same way with key-as-segment:The result will be
Path[(EntitySet: Schools)/(Key: SchoolID = 1)/(NavigationProperty: Student)/(Type: Collection([Microsoft.Test.Taupo.OData.WCFService.Customer Nullable=False]))]
. -
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 repsonse. Every time an expanded navigation property is written, the full expanded feed or entity should be written instead of just the delta changes because in this way it’s easier to manage the association among entities consistently. Inside the expanded feed or entity, there are ONLY normal feeds or entities. 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 feed (collection of entities) in a delta response. Please note that regardless of whether or not the navigation links will be eventually written to the payload,
WriteStart(navigationLink)
MUST be called before actually callingWriteStart(expandedFeed)
to write an expanded feed. So is for a single expanded entity.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(entry)
,ODataJsonLightDeltaWriter
keeps track of an internal state machine thus can correctly differentiate between writing a delta entry and writing a normal entry. Actually during writing the expanded navigation properties, all calls toWriteStart(entry)
,WriteStart(feed)
andWriteStart(navigationLink)
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 entry but will skip writing any structural property or instance annotation until it begins to write a navigation link (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.ExpandedNavigationProperty
. 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 feeds and entries 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 eachODataEntry
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 feed 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 theExpandedNavigationProperty
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
7. DESIGN
-
7.1 Parser Extension Design
This doc discuss about the design of extension of Path parser part of UriParser. 1 Path Parser Overview
1.1 Current path parsing logic
- Linear parsing, one segment is determined, then next one;
- Syntax and semantic passes are combined together, as we need type information from model to validate the identifiers (Web API );
- For certain raw segment, each kind of segment is tried one by one. And it stops processing when one matches;
- Exception thrown immediately when unknown segment encountered.
Related code: ODataPathParser.cs
1.2 KeyAsSegment Rule
Key as segment means user could choose to place a key in the path, instead of in brackets, which is OData convention. Currently the ODataLib supports KeyAsSegment by providing an ODataUrlConventions setting on ODataUriparser. But the introducing of KeyAsSegment does bring conflicts, in order to solve those conflicts, ODataLib also introduced the ‘$’ escape sign. Here are some words taken from source code:
In this case for the following 2 Urls would have different parsing results: 1.
/Me/Messages/ns.Message {Singleton}/{Navigation}/{Key}
2./Me/Messages/$/ns.Message {Singleton}/{Navigation}/{Type}
As the quote says, for now we still do not have document to describe the detailed behavior of ‘$’, so it would be nice if we can have some pre-defined parsing rules to resolve the confliction while not relying on ‘$’. Also it is supposed to be the default Url convention for OData simplified. Detailed design would be discussed in later part.
1.3 Design goal
- Do not throw exception until segment cannot be handled eventually, customer extensions may define new syntax rules.
- Modularize single segment parser, we easily integrate community contributed parser extensions. (We’d provide an extension framework, instead of hard-coded extensions.)
2 Design Detail
2.1 New parser context class
Add the following class:
2.2 Update Parser configuration
Modify the following class:
User could choose to modify ParsedSegments in ODataUriParserContext, in order to custom path parser behavior. For implementation wise, we should first modularize current path parser, and change various TryCreateXXXSegment into some method working with the ODataUriParserContext. That means the Path parser now becomes a parsing flow runner with extensions for users to implement, while the default implementation is the old parsing behavior.
Please note the Uri string segment and parsed path segment do not necessarily have an one-to-one mapping. One thing to note is that we do not enforce forward-only parsing flow for current Uri Parser, that means a later action could delete a previous parsed segment in the upcoming steps. For example, when parsing the following Uri string:
~/People(1)/Orders/$ref
When parser reaches ‘Orders’, it would recognize it as a navigation property segment on people, and add the new navigation segment to the ParsedSegments list. Later when it meets the ‘$ref’ keyword, it knows that the Uri is to address a reference link instead of the navigation target directly. Unfortunately, we’ve got different segment kinds for the two (NavigationPropertySegment and NavigationPropertyLinkSegment). In this case, the parser would remove the current tailing NavigationPropertySegment segment, and then add a new NavigationPropertyLinkSegment to the parsed list. For this case the expected behavior is: when the parser reaches ‘Orders’, it do a pre-peek at the coming up segment, and decide whether to add a new NavigationPropertySegment or NavigationPropertyLinkSegment. The current behavior doesn’t look like an idea model for path parsing, while a non-backtracking parser would be more intuitive both for the parsing flow and the extension providers. If we are going to change this, we may have a simplified parser context and parsing flow.2.3 New Convention for KeyAsSegment
When ODataSimplified convention is chosen, the following path segment parsing order is applied:
collectionNavigation singleNavigation Fixed Segment ( $ref, $count) Fixed Segment ($ref, $value) Type Property Operation Type Key Operation -
7.2 Untyped JSON Design
This doc describes the design and implementation of untyped JSON payload support in reader/writer of ODataLib for OData V4. 1. Design Overview
1.1 Definition
OData JSON Format defines a complete specification for the JSON format used by OData payload. It conforms to the original JSON format, but adds some restrictions, thus limitations. Those restriction rules were brought in order to support the interoperability of OData. Some customers may want to carry the custom JSON data in the payload. In this case, the result payload is still a valid JSON, but goes against the OData JSON Format Spec, thus it could not be understood by ODataLib, neither could ODataLib write such sort of payloads.
1.2 Key notes for design
Basically we’d have the following principles for untyped JSON feature: 1. ODataLib should be able to recognize valid JSON object which doesn’t meet OData spec 2. ODataLib should provide a built-in representative for such JSON objects 3. Current reading/writing behavior should not be affected, that includes but not limited to, user should be able to expect exception when extra untyped JSON was found. Based on this, we’d add a new flag for supporting untyped JSON, and a new class to represent such JSON object.
2 Design Detail
2.1 New enum flag to support untyped JSON
2.2 New class to represent untyped JSON element
2.3 Reader support
Property kind: known odata.type
- Example
- Behaviour
Default Ignore Untyped JSON ————————————— ——————————— ———— Exception(Non-open), Read (Open) Ignore(Non-Open), Read (Open) Read Property kind: no odata.type with Primitive value
- Example:
- Behaviour
Default Ignore Untyped JSON ————————————— ——————————— ———— Exception(Non-open), Read (Open) Ignore(Non-Open), Read (Open) Read Property kind: Unknown odata.type
- Example
- Behaviour
Default Ignore Untyped JSON ————————————— ——————————— —————- Exception Ignore Read as untyped Property kind: no odata.type with non-primitive value
- Example
- Behaviour
Default Ignore Untyped JSON ————————————— ——————————— —————- Exception Ignore Read as untyped Code Change
Update the following part: ODataJsonLightEntryAndFeedDeserializer:ReadUndeclaredProperty And add the parsing logic
2.4 Writer support
This matrix is symmetric to the reader’s.
Property kind: Known odata.type
- Example
-Behaviour
Default Ignore Untyped JSON ————————————— ——————————— ———— Exception(Non-open), Write (Open) Ignore(Non-Open), Write (Open) Write Property kind: no odata.type with Primitive value
- Example
-Behaviour
Default Ignore Untyped JSON ————————————— ——————————— ———— Exception(Non-open), Write (Open) Ignore(Non-Open), Write (Open) Write Property kind: Unknown odata.type
- Example
-Behaviour
Default Ignore Untyped JSON ————————————— ——————————— ———————- Exception Ignore Write as untyped JSON Property kind: no odata.type with non primitive value
- Example
-Behaviour
Default Ignore Untyped JSON ————————————— ——————————— ———————- Exception Ignore Write as untyped JSON Code Change
For now, ODataUndeclaredPropertyBehaviorKinds is used in Reader only, should also add the following property in ODataMessageWriterSettings.
ODataJsonLightPropertySerializer::WriterProperty should be able to support writing the new ODataUntypedValue instances.
-
7.3 Navigation Property in Complex Type Design
~Inital draft, Improve frequently~ 1 Design Summary
1.1 Overview
This doc describes the design about supporting the navigation property in complex type. It is related to the following components/libraries:
- OData CSDL & Edm
- OData Core
- OData Client
- Web API
1.2 Goals/Scopes
- Edm
- Construct and validate navigation property in complex type from EdmModel.
- Define navigation property in complex type, include the navigation property binding.
- Read/write navigation property in complex type from/to CSDL.
- Core
- Parse navigation property in complex type in UriParser.
- Serialize/Deserialize navigation property in complex type from/to payload.
- Client
- Track complex type with navigation property.
- Gen navigation property in complex type codes.
- Web API
- Build EdmModel with navigation property in complex type.
- Provide the routing logic for navigation property in complex type.
- Serialization/ Deserialization navigation property in complex type in payload
1.3 Non-Goals
- Containment navigation property in complex type.
- Dynamic navigation property in open type
- Update navigation property in Collection complex property
2 Design Details
2.1 CSDL and EDM
2.1.1 Construct navigation property in complex type
From OData Spec:
- Entity types are named structured types with a key. They define the named properties and relationships of an entity.
- Complex types are keyless named structured types consisting of a set of properties.
So, both entity types and complex types are structured types with properties, include declared properties and navigation properties. Below picture shows the class relationship between complex type and entity type, navigation property and structural property in ODataLib so far.
From above picture, we can find that the DeclaredProperties of IEdmStruturedType is defined as a List of IEdmProperty, which can hold either the navigation property or the structural property. So from interface perspective, the navigation property in complex type is supported without any change.
However, we need to do as follows to allow the customer to define navigation property on complex type:
1 Promote the following public/private APIs from EdmEntityType to EdmStructuredType.
Then, developers can call as follows to define the navigation property on complex type, for example:
2 Modify
EdmNavigationProperty
class.- a) Change the private constructor to accept the IEdmStructuredType.
- b) Change DeclaringEntityType property as following:
- c) Add a similar property named DeclaringComplexType, the implementation is same as #b.
- d) Add new public APIs as follows:
- e) Make the original CreateNavigationProperty() function and new added public API for complex type to call the new added private function, same as follows:
3 Add the following extension methods:
2.1.2 Write navigation property in complex type in CSDL
There is a logic to write the complex type and its declared properties. We can add the navigation properties writing logic after writing declared properties. So, We should change the function ProcessComplexType() in EdmModelCsdlSerializationVisitor class as follows to write navigation properties in complex type:
Then, the complex type in metadata document may have navigation property. Let’s have an example:
2.1.3 Read navigation property in complex type in CSDL
Reading/Parse the navigation property in complex type is a lit bit complex. We can analysis the entity type and complex type class inheritance in CSDL. Below picture shows the class relationship between CSDL complex type and CSDL entity type. Both are derived from CsdlNamedStructuredType, then derived from CsdlStructuredType:
So, we should modify as follows:
- Promote everything about the navigation property from CsdlEntityType to CsdlStructuredType.
- Modify the constructors of CsdlStructuredType, CsldNamedStructuredType, CsdlComplexType to accept the navigation properties. For example:
- Modify CreateRootElementParser() function in CsdlDocumentParser. Add the following templates for Complex type element:
- Modify CsdlSemanticsNavigationProperty class to accept CsdlSemanticsStructuredTypeDefinition.
- Override the ComputeDeclaredProperties() function in CsdlSemanticsComplexTypeDefinition
2.1.4 Construct navigation property binding in complex type
OData spec says:
From the highlight part, we can find that the property path is necessary for navigation property binding in complex type. So, we should save the property path for navigation property in complex type. Let’s have an example to illustrate the property path for navigation property in complex type.
The binding path of the navigation property “City” of entity set “Customers” should be “Location/NS.Address/City”. Or we can just remove the type cast if it is not the sub type as “Location/City”. As a result, we should add a new public API for EdmNavigationSource class to let customer to define the property path for navigation property in complex type:
Let’s have a detail example to illustrate how the users (developers) to add the navigation binding:
1) Add a complex type:
2) Add “Customer” entity type:
3) Add “City” entity type
4) Add a navigation property for “Address” complex type
5) Add an entity set and the navigation property binding
6) Therefore, we can have the navigation property binding as:
2.1.5 Validation rules for navigation property in complex type
There’re a lot of validation rules related to navigation property, entity type and complex type. So, we should:
- Remove the old validation rules which disallow the navigation property in complex type. If any, we have to remove the validation methods from active rules. We can’t remove the validation methods because they are public.
- Add new validation rules for navigation property in complex type, since it is a bit different with navigation property in entity type, including:
- Partner MUST NOT be specified for navigation property of complex type, according to the spec.
- ContainsTarget is not true for navigation property of complex type, since we are not going to enable it now.
2.2 OData Core
2.2.1 Uri Parser
The navigation property in complex type can be in path/segment or query option. We have to support all of these. Let’s see some Uri templates for navigation property in complex type:
- Navigation property segment of complex type:
- ~/entityset/key/complexproperty/navigation
- ~/entityset/key/complexproperty /…/navigation
- ~/entityset/key/complexproperty /…/navigation/property
- ~/entityset/key/complexproperty /…/navigation/$count (for collection)
- Function/action after navigation property segment of complex type:
- ~/entityset/key/complexproperty /…/navigation/boundfunction
- ~/entityset/key/complexproperty /…/navigation/boundaction
- Navigation property in complex type in query options
- $expand=property/navigationproperty
- $select=property/navigationproperty
2.2.1.1 Parse Path Segments
We can use the existing classes NavigationPropertySegment and NavigationPropertyLinkSegment to represent the navigation property of complex type without any change.
However, we should pass the previous navigation source from structural property to the navigation property belong to this. So, we should change the CreatePropertySegment() function in ODataPathParser to save the previous navigation source in property segment as follows:
Let’s have a request example:
Then, the result of Uri parser can be:
2.2.1.2 Parse query option
So far, SelectExpandBinder only supports the following expand clause: * $expand=NavigationProperty[,…] * $expand=TypeCast/NavigationProperty/$ref * …
As navigation property be allowed in complex type, the SelectExpandBinder should support more expand and select clauses as follows: * ~/../ complexproperty?$select=navigation * $expand= complexproperty /navigation * $expand= complexproperty /typecast/ navigation * ~/../ complexproperty?$expand=navigation * ~/../ complexproperty?$expand=navigation
So, we should modify the codes as follows:
- Add a flag for Uri resolver to configure whether the navigation property is allowed in complex type
- Modify GenerateExpandItem(ExpandTermToken) function in SelectExpandBinder
- Modify SelectEpxandPathBinder to add a new function to process the property segment in expand clause.
Let’s have an example: ** $expand=Location/City **
Then, selectAndExpand has one SelectedItems with the following OData path with segments:
- PropertySegment
- NavigationPropertySegment
2.2.2 Serialize navigation property in complex type payload
#### 2.2.2.1 Serialization process
Let’s take a look about the serialization flow about navigation property in entity type. Below picture shows the simple flow about the serialization of entry, includes a) entry without expanded navigation property, b) entry with expanded navigation properties.
From the picture, we can find that the serialization flow is more complicated if entry with expanded navigation property. Moreover, there’s no way to expand the navigation property in complex type property, owing that the complex type property is serialized completely in WriteStart process for entry same as other structural properties. So, as navigation property be allowed in complex type, we should stop the write process for entry once a complex type property with navigation property is met. Then, we can use the process same as navigation property in entity type to write the complex property with expanded navigation property. So far, we have the following proposal, (other proposal please refer to appendix): Create a new class, for example ODataExpandableProperty, and add write start API on this class. For example:
2.2.2.2 Single expandable property in entry
Let’s see how to serialize the entry with complex type property in which the navigation properties are expanded. Based on the above proposal, the basic serialization flow for entry with expandable property with expanded navigation property should be as follows:
The corresponding server side codes to serialize the navigation property in complex type should be as:
So, we can do as follows: 1. Create a new class
Basically, ODataExpandableProperty can have the same structure of ODataProperty, but it should be derived from ODataItem, or it can be embedded with ODataProperty. 2. Add two new abstract APIs in ODataWirter
The users (developers, service) can call these APIs to write the expandable property. 3. In ODataWriterCore, give an implementation for the above new abstract APIs.
4. Add new item in WriterState enum type:- Add two new abstract API as follows in ODataWirterCore
Then, to implement them in ODataAtomWriter & ODataJsonLightWriter. So far, leave the implementation in ODataAtomWriter to throw NotImplementedException.
- In ODataJsonLightWriter, we can have the following prototype codes:
- In OdataJsonLightPropertySerializer, add new internal API WriteExpandableProperty(…) to write the structural properties of the expandable property.
- Modify the related write scope to make parent of navigation property scope can be expandable property.
Let’s have an example:
Where the model schema can be: * Entity type “NS.Order” has a complex property named “Location” with “NS.Address” complex type * “NS.Address” has a navigation property named “City” with “NS.City” entity type.
So, we can construct all related object as follows:
Then, we can write the navigation property in complex type as:
We can have the following payload:
2.2.2.3 Collection expandable property in entry
We can reuse the ODataExpandableProperty class to serialize the collection expandable property. For example:
Then the service side codes can be as follows:
2.2.2.4 Top level expandable property
The API ODataMessageWriter.WriteProperty() is used to write property without navigation property. To support navigation property in complex type, we can’t use it, because it cannot be used to write expanded entry and feed. Similar to delta entry writer, we should have the following classes to support top level expandable property with navigation property:
And make the implementation in a new class as follows:
2.2.2.5 Top level collection of expandable property
Similar to ODataCollectionWriter, we can provide ODataCollectionExpandablePropertyWriter to writer the top level collection of expandable property.
2.2.3 Deserialization navigation property in complex type payload
2.2.3.1 Deserialization process
The deserialization, or parse payload, or read payload is a process to covert the payload string into OData object, for example, ODataEntry, ODataProperty, etc. The process uses a state to track the reading. So, there are many read states transferred from one to anther in one deserialization process. Let’s have look about the basic entry payload deserialization.
2.2.3.2 Single expandable property in entry
Simply input, we should add two states, for example:
We will only stop and return such state when we reading property with expanded entry in it. So, the server side can have the following structure to catch the state and figure out the expandable property.
Based on this design, we should do as follows: 1. Add the following APIs in ODataReaderCore to start and end reading the expandable property.
- Need a new Scope to identify the expandable property reading
2.2.3.3 Collection expandable property in entry
For collection, it’s same as single expandable property, except that the embed property should have the collection value.
2.2.3.4 Top level expandable property
The API ODataMessageReader.ReadProperty() is used to read property without navigation property. To support navigation property in complex type, we can’t use it, because it cannot be used to reader expanded entry and feed. Similar to delta entry reader, we should have the following classes to support top level expandable property with navigation property:
And the implementation:
2.2.3.5 Top level collection of expandable property
Similar to ODataCollectionReader, we can provide ODataCollectionExpandablePropertyReader to writer the top level collection of expandable property.
2.3 OData Client
### 2.3.1 Client Support Operation on Complex type Scenario: Suppose that we have type Location, Address, City. Location, City are Entity type, Address is Complex Type. City is the navigation of Address. Then supposedly customers can use following APIs on complex type with NP (navigation property) on client. 1. LINQ Expand
- LoadProperty
- AddRelatedObject, UpdateRelatedObject
- AddLink, SetLink, DeleteLink NOTE: 2,3,4 are not applicable to collection value complex type.
2.3.2 Materialization
Materialization happens by converting the response payload to client object. Client defines different materializers to materialize different kind of payload. As shown in following picture: * ODataEntitiesEntityMaterializer: Handle the response of SaveChanges() * ODataReaderEntityMaterializer: Handle response of querying Entry or Feed, and not queried through LoadProperty, e.g. GET ~/Customers * ODataLoadNavigationPropertyMaterializer: When LoadProperty is called * ODataCollectionMaterializer: Handle response of querying collection value property * ODataValueMaterializer: Handle response of querying a value, e.g. GET ~/Customers/$count * ODataPropertyMaterializer: Handle response of querying a property, e.g. GET ~/Customers(1)/Name * ODataLinksMaterializer: Handle response of querying the reference links e.g. GET ~/Customers(1)/Orders(0)/$ref
The common process of a query is:
The Materialization (Part 2) is driven at the top level by an instance of MaterializeAtom, which implements the enumerable/enumerator. The materializer reads OData object from payload with ODataReader and materialize by calling different materialization policy and tracks materialization activity in an AtomMaterializerLog. Then MaterializeAtom instance applies AtomMaterializerLog onto the context (entityTracker) for each successful call to MoveNext(). During an entry materialization, MaterializerEntry/MaterializerFeed/MaterializerNavigationLink will be created to record the materializer state for a given ODataEntry, ODataFeed and NavigationLink respectively.
2.3.2.1 Materialization class for complex type with navigation property
As complex type with navigation property will be read as an ODataExpandableProperty in ODataReader. To align with this: 1. Add ExpandablePropertyMaterializationPolicy to be responsible for materializing an ODataExpandableProperty.
- Add MaterializerExpandableProperty to remember the materializer state of a given ODataExpandableProperty.
2.3.2.2 Materialize complex type property in an entry
When payload is an entry or a feed, ODataReaderEntityMaterializer will be created to materialize the response. So we need add logic in ODataReaderEntityMaterializer to handle the complex type with navigation property. 1. Add ICollection complexProperties to MaterializerEntry. In Materializer.Read(), add state ODataReaderState.ExpandablePropertyStart/ ODataReaderState.ExpandablePropertyEnd to read complex type and its navigation property to complexProperties. And for each complex type having navigation property, create an instance of MaterializerExpandableProperty. Following is a sample:
Then the data flow would be like:
- Materialize The materializer will call EntryValueMaterializationPolicy to materialize an entity, and in EntryValueMaterializationPolicy, we can call ExpandableComplexPropertyMaterializationPolicy to handle complex type having navigation property.
- ApplyLogToContext Update the materialization info to DataServiceContext. Will explain more in tracking section.
2.3.2.3 Materialize top level complex type property
Currently top level single-value complex type is handled by ODataPropertyMaterializer, and collection-value complex type is handled by ODataCollectionMaterializer, and the readers are:
- ODataPropertyMaterializer ODataMessageReader.ReadProperty
- ODataCollectionMaterializer ODataMessageReader. CreateODataCollectionReader
For complex type has navigation property, we have separate reader for it.
- Single-value complex type ODataExpandblePropertyReader
- Collection-value complex type ODataCollectionExpandablePropertyReader
So in ODataPropertyMaterializer/ODataCollectionMaterializer we need add logic to read with ODataExpandblePropertyReader/ODataCollectionExpandablePropertyReader when we found the complex type has navigation property (by visiting the model).
2.3.2.4 Materialize LoadProperty under complex type property
When LoadProperty is called, ODataLoadNavigationPropertyMaterializer will be used as materializer. So we need add logic for complex type in this materializer:
- Get existing complex type instance from descriptor (Refer to the tracking part)
- Read the payload to ODataEntry or ODataFeed
- Materialize the ODataEntry/ODataFeed and set it as property of the complex type instance
2.3.3 Tracking complex type
2.3.3.1 Existing Entity Tracking
In order to directly have operations on a materialized entity, we need store the needed info internally in order to generate the Url for a real http request. For example, company has been materialized to a clr object, and we try to update a property by directly modifying the clr object.
In this case, we need try to get the company editlink in order to send PATCH against it. And info like editlink can be achieved during company materialization. For this reason, client will create an EntityDescriptor when materializing an entity, and store the mapping of the materialized entity and its descriptor in EntityTracker. And the entitytracker can be accessed through DataServiceContext.
EntityDescriptor is defined as:
And in EntityTracker we have:
- Dictionary<object, EntityDescriptor> entityDescriptors: The mapping of entity clr object and entity descriptor. With the entity clr object, we can search the dictionary to get its EntityDescriptor, then we can get the editlink/selflink of the entity, and send quest against it.
- Dictionary<Uri, EntityDescriptor> identityToDescriptor The mapping of entity id and entity descriptor. When materializing an entity, we will firstly search this dictionary to check if the entity is already been tracked (materialized). If it is already been materialized, it will reuse existing clr object (EntityDescriptor-> entity), and apply new values to it.
- Dictionary<LinkDescriptor, LinkDescriptor> bindings The binding of entity and its navigation property which has been tracked (materialized). For example, if we request Get Customers(1)?$expand=Order, then during materialization, we will create a LinkDescriptor(customer(id=1), “Order”, order, this.model) to track the binding, customer and order is the materialized object. This dictionary can be used for AddLink, SetLink…
So when we try to query an entity, client will work as:
2.3.3.2 Complex type tracking
Like entity, in order to support LoadProperty, AddRelatedObject, AddLink… on complex type, we need track as well for those complex type having navigation property. But as we do not have an identity for complex type, so we can only track single-value complex type, and the complex type property must be queried inside an entry (Will explain why we has this restriction later).
- Create ComplexTypeDescriptor In order to reuse current logic of EntityDescriptor, we can add a base class ResourceDescriptor let EntityDescriptor and ComplexTypeDescriptor inherit from it.
- Then for EntiyTracker, it will be like:
- Then track when complex type property is queried through entry Materializer.Read(): a. Create ComplexTypeDescriptor while encountering complex type property which is needed to be tracked, complex type identity consists of entity id plus complex property name, for example, ~/Locations(1)/Address. b. Update relatedLinks in ComplexTypeDescriptor by reading navigation links in complex type property (Need ODL support: ODL reader should be able to compute the navigation links for navigation property under single-value complex type). c. Add ComplexTypeDescriptor to EntityDescriptor d. If the navigation link of complex type is expanded in the payload, read the expanded entry or feed, and annotate the navigationlink with MaterializerNavigationLink, same with reading the navigation property of entity.
Materializer.Materialize: Materialize OData value to client clr object.
a. Update materialized value of complex type property to complexTypeDescritors in EntityDescriptor b. Create a LinkDescriptor of the materialized complex value and its materialized navigation property value and add it to MaterializerLog.Materializer.ApplyLogToContext(): Update entity tracker based on previous materialization. a. Add or merge EntityDescriptor to EntityTracker, including updating complexTypeDescriptors in EntityDescriptor b. Add or merge each complexTypeDescriptors in EntityDescriptor to Dictionary of complexDescriptors and identityToDescriptors in entityTracker. c. Update complex type navigation links in MaterializerLog to bindings of entityTracker.
- We cannot track collection-value complex type property, so following scenario does not work:
Reason: We do not have an identity for a complex type instance in a collection, and there is no way for us to know if the incoming complex type has been materialized before. We probably can support this tracking if we allow indexing into collections.
Workaround: Expand city instead, for example,
context.Locations.Bykey(1).Addresses.Expand(a=>a.City)
;- We cannot track if complex type property is queried as individual property For example, in following scenario, we are not able to track complex type and use it in LoadProperty:
Reason: When querying complex type property directly, we do not have the entity info. We only have the request Uri and the request Uri cannot be used to identify a complex type. For example, Get ~/Locations(1)/Address and Get ~/Company/Location/Address may actually get the same instance.
Workaround: Query the entity which includes the complex type.
2.3.4 Public API Implementation related to tracking
- LoadProperty For example, when customer call context.LoadProperty(address, “City”), client need add logic:
- Remove the validation about address must be an entityType
- Search dictionary of ResourceDescriptors with object address to get the ComplexTypeDescriptor
- Get the navigationlink of City from ComplexTypeDescriptor-> relatedEntityLinks
- Generate request with the navigationlink of City
- AddRelatedObject, UpdateRelatedObject
- Remove the validation about source
- Update the parent Descriptor of EntityDescriptor to ResourceDescriptor so it can accept ComplexTypeDescriptor
- For AddRelatedObject, add the LinkDescriptor to the bindings in entityTracker
- AddLink, SetLink, DeleteLink Similar to Entity, add/set/delete bindings of entitytracker, and get source/target descriptor to generate request.
2.3.5 Serialization
When we try to add or update an entity from client, we need call ODataWriter to serialize the object to payload. So in class Serializer: 1. WriteEntry If we do not do any operation to the navigation property under complex type, then this function does not need any change. Meanwhile, if we want to support adding bindings to complex type (only for single-value complex type), like the following scenario:
Then the process would be like: a) AddToLocations would create an EntityDescriptor for location and add it to entityTracker.ResourceDescriptor b) SetLink would create a LinkDescriptor between address and city and add it to entityTracker.bindings c) In BaseSaveResult->RelatedLinks (EntityDescriptor entityDescriptor) which is try to enumerate the related Modified/Unchanged links for an added item, try to match link.source to the entity or property value in the descriptor. If the link.source is equal to a property value, create a ComplexTypeDescriptor and add it to entityDescriptor and entityTracker. d) When creating OData objects, go through ComplexTypeDescriptor under EntityDescriptor and create ODataExpandableProperty from them e) Write ODataEntry, and for ODataExpandableProperty, call WriteStart(ODataExpandableProperty property), and call WriteEntityReferenceLink to write the binding.
- WriteEntityReferenceLink When AddLink/SetLink is called on complex type, this function will be called to write the payload. So the function need update the logic from EntityDescriptor to ResourceDescriptor.
2.3.6 CodeGen
In order to support .Expand on complex type, we need generate DataServiceQuery for Complex Type. For scenario Locations->Address->City, Address is complex type, City is the navigation property of Address:
- Generate Class for Address:
If City is single-value navigation property:
If City is collection-value navigation property:
- Add Address to Location: If complex type Address is single-value:
Then we can support:
If complex type Address is collection-value:
Then we can support:
- Add Address to LocationSingle: If complex type Address is single-value:
Then we can support:
If complex type Address is collection-value:
Then we can support:
2.4 Web API OData
2.4.1 Model builder
Similar with the entity type and complex type structure, Web API OData has the same configuration class structure. Below picture shows the relationship between complex type configuration, entity type configuration and structural type configuration.
Owing that NavigationPropertyConfiguration is derived from PropertyConfiguration, and all properties for type (entity type or complex), either structural properties, or navigation properties are saved in the following dictionary in StrucutralTypeCofiguration:
So, form this point, complex type configuration can support navigation property. However, we need to do as follows to allow the customer to define navigation property on complex type:
- Promote the following public/private APIs from EntityTypeConfiguration to StructuredTypeConfiguration.
- Promote the following property from EntityTypeConfiguration to StructuredTypeConfiguration
- Modify NaviatonPropertyConfiguration class, for example
- modify DeclaringEntityType property
- add DeclaringComplexType property
- modify the constructor
- …
- Modify EdmTypeBuilder class to construct the complex type with navigation property.
- Promote the following APIs from EntityTypeConfigurationOfTEntityType to StructuralTypeConfigurationOfTStrucuturalType.
Let’s have an example to illustrate how configure the navigation property in complex type: a) We have the three types, Customer and Region as entity type, Address as complex type
Then, we can configure the Edm type by non-convention model builder as:
2.4.2 Convention model builder and conventions
In convention model builder, it is assumed that complex type can’t have navigation property. As a result, the properties type belong to complex type is built as complex type if it’s not enum type or primitive type. As navigation property is allowed in complex type, we should change the flow to assume the structural type of property in complex type as entity type. Once all types are buil, we should re-use the re-discover logic to change the assumed type. So, we should do:
- Modify MapComplexType(…) function, for any implicated added structural types from complex type, mark it as entity type and add them as navigation properties.
- Re-configure the properties in complex type if the related entity types are re-configured as complex type.
For user codes, it should be same as previous, for example we re-use the CLR classes mentioned in previous section:
Use the above codes, the Region type should be built as entity type automatically.
2.4.3 Navigation Source binding for navigation property in complex type
We should modify some codes in NavigationSourceConfigurationOfEntityType to make navigation source binding to the navigation property in complex type: For example:
Make sure the property path can be saved correctly.
2.4.4 Navigation property routing
- Path segment The navigation path segment class NavigationPathSegment can be re-used for navigation property in complex type. However, the GetNavigationSource() in PropertyAccessPathSegment can’t return null directly. It maybe return the previous navigation source if the property is complex type.
- Routing convention
We can provide the one level routing convention for navigation property, but leave other for attribute routing. We can add the following path template in NavigationRoutingConventions:
- ~/entityset/key/property/navigation
- ~/entityset/key/property/cast/navigation
- ~/singleton/property/navigation/$count
- ~/singleton/property/cast/navigation/$count
The convention action name can be:
“RequestMethodName” + “NavigationPropertyName” + “In” + “ComplexPropertyName” + “From” + “DeclareTypeName”
For example, GET ~/Customers(1)/Location/Region
The action name can be “GetRegionInLocationFromCustomer”
- Query option
Change the SelectExpandBinder to support expanding the navigation property in complex type. For example:
- ~/…/Property?$expand=navigation
- ~…?&expand=property/navigation
2.4.5 Serialization navigation property in complex type
2.4.5.1 Expand complex property in Entry
- SelectExpandNode In SelectExpandNode, we have the following sets:
- SelectedStructuralProperties
- SelectedNavigationProperties
- ExpandedNavigationProperties
These sets are enough to expand a navigation property in entity, but it’s non-enough to expand a navigation property in complex type. Because, we should know which complex property is expanded. That’s, if we have the following request:
We should know “Location” is an expandable property and it’s expanded with “Region”. So, for SelectExpandNode, we should at least a set to save the expanded structural property. Let’s say it can be:
And we should construct this property in constructor of SelectExpandNode class.
- Entit type serializer
For entity type serializer, we can use the ExpandedStructuralProperties defined in SelectExpandNode to construct the expandable property. So, we should do: a) In CreateEntry function, before we call CreateStructuralPropertyBag() function, we should remove the expanded structure properties from SelectedStructuralProperties, and use the except set to build the properties for entry.
Then, the expanded structural properties exclude from the properties.
b) Provide a new private API to write the expanded structural properties:
c) Call WriteExpandedStructuralProperties after WriteStart(entry)
2.4.5.2 Top level expanded complex property
We can modify ODataComplexTypeSerializer to support expanded complex property. For example:
So, we can do as follows: 1. Modify EntityInstanceContext to accept complex type instance, or create a new class named ComplexInstanceContext.
- Use the SelectExpandNode into ODataComplexTypeSerializer
- If ExpandedStructuralProperties is empty, serialize the complex as normal, otherwise we will serialize an expandable property.
- Add new function named CreateExpandableProperty
- Create a new function to write the expandable property
Where, WriteExpandedNavigationProperties maybe same as the function in ODataEntityTypeSerializer. For top level collection of expandable property, we can use the above same logic but create an ODataCollectionExpandablePropertyWriter to write.
2.4.6 Deserialization navigation property in complex type
2.4.6.1 Expand complex property in Entry
As mentioned in OData Core for reader, we have two new reader state:
So, we can use them to read the expandable property. 1. Create a new class named ODataExpandablePropertyWithNavigationLinks
- Modify ReadEntryOrFeed() function in ODataEntityDeserializer by added two case statements into:
In ExpandablePropertyStart, we can create ODataExpandablePropertyWithNavigationLinks object to push it into Stack. Make sure, we should modify the state transfer to make sure the NavigationState can follow up ExpandablePropertyStart. 3. Should modify ODataEntryWithNavigationLinks to accept the ODataExpandablePropertyWithNavigationLinks
- Add new API named AddExpandedStrucutralProperties()
And call it brefore ApplyNavigationProperties in ApplyEntityProperties().
2.4.6.2 Top level expanded complex property
Web API doesn’t support to read a top level complex property. So, we shouldn’t support expanded complex property.
2.4.6.3 Expanded complex property as action parameter
TBD.
8. TOOLING
-
OData Client Code Generation Tool
OData provide two tools to generate client proxy file for an OData Service. - OData Client Code Generator support generating client proxy file for OData V4 Service. It supports following Visual Studio:
- Visual Studio 2010 (The last version of this tool for VS 2010 is 2.3.0, You can download it from ODataItemTemplate.2.3.0.vsix.)
- Visual Studio 2012
- Visual Studio 2013
- Visual Studio 2015
For full documentation, please refere to “How to use odata client generator to generate client proxy file”.
- OData Connected Service lets app developers connect their applications to OData Services (both V3 & V4) and generate the client proxy files for the services. It supports following Visual Studio:
- Visual Studio 2015
The following part will mainly focus on how to use the OData Connected Service to generate client proxy file.
Install OData Connected Service Extension
You can install this extension by this link from vs gallery. Or, you can install it in Visual Studio 2015.
In Visual Studio, Click Tools > Extensions and Updates.
Expand Online > Visual Studio Gallery > Tools > Connected Service, and select the OData Connected Service extension.
Click Download.
Then it will pop up a VSIX Installer window, Click Install.
Click Close once the installation finishes.
You need to restart the visual studio in order for the installation to take effect.
Generate Client Proxy
Create a new project
Create your project. Here, we take “Console Application” project as an example.
Start Visual Studio and from the File menu, select New and then Project.
In the Templates pane, select Installed > Templates, expand the Visual C# > Windows > Classic Desktop and select Console Application. Name the Project “TrippinApp” and click OK.
Generate client proxy for an OData service
In the Solution Explorer pane, right click the “TrippinApp” project and select Add and then Connected Service.
In the Add Connected Service dialog, select OData and then click Configure.
In the Configure endpoint dialog, input the service name and the OData service endpoint, then click Next button.
In the Settings dialog, enter the file name(without extension) of the proxy file and click Finish.
In the Settings dialog, You also can configure some other settings by click AdvancedSettings link. Then you can set the related code generation settings.
Once you finished all those settings, click Finish. This tool will begin to install the related NuGet packages and generate the client proxy file into your project.
Consume the OData service
Now, the developer can write client code to consume the OData Service.
using System; using Microsoft.OData.SampleService.Models.TripPin; namespace TrippinApp { class Program { static void Main(string[] args) { DefaultContainer dsc = new DefaultContainer( new Uri("http://services.odata.org/V4/(S(fgov00tcpdbmkztpexfg24id))/TrippinServiceRW/")); var me = dsc.Me.GetValue(); Console.WriteLine(me.UserName); } } }
Summary
Now you have the OData Connected Service at your disposal to generate your client proxy for any OData service. To leave us feedback, please open github issues at OData Lab GitHub.
- OData Client Code Generator support generating client proxy file for OData V4 Service. It supports following Visual Studio:
I have noticed you don’t monetize zhuoyue.me, don’t
waste your traffic, you can earn additional cash every
month with new monetization method. This is the best adsense alternative for
any type of website (they approve all websites), for more details simply search in gooogle: murgrabia’s tools