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.

    IEdmModel model = builder
                    .BuildAddressType()
                    .BuildCategoryType()
                    .BuildCustomerType()
                    .BuildDefaultContainer()
                    .BuildCustomerSet()
                    .GetModel();

    Then set up the message to write the payload.

    MemoryStream stream = new MemoryStream();
    InMemoryMessage message = new InMemoryMessage() {Stream = stream};

    Create the settings:

    ODataMessageWriterSettings settings = new ODataMessageWriterSettings();

    Now we are ready to create the ODataMessageWriter instance:

    ODataMessageWriter writer = new ODataMessageWriter((IODataResponseMessage) message, settings, model);

    After we write the payload, we can inspect into the memory stream wrapped in InMemoryMessage to check what is written.

    string output =Encoding.UTF8.GetString(stream.ToArray());
                Console.WriteLine(output);
                Console.Read();

    Here is the whole program that use SampleModelBuilder and InMemoryMessage to write metadata payload:

    IEdmModel model = builder
                    .BuildAddressType()
                    .BuildCategoryType()
                    .BuildCustomerType()
                    .BuildDefaultContainer()
                    .BuildCustomerSet()
                    .GetModel();
    
                MemoryStream stream = new MemoryStream();
                InMemoryMessage message = new InMemoryMessage() {Stream = stream};
    
                ODataMessageWriterSettings settings = new ODataMessageWriterSettings();
    
                ODataMessageWriter writer = new ODataMessageWriter((IODataResponseMessage) message, settings, model);
                writer.WriteMetadataDocument();
    
                string output =Encoding.UTF8.GetString(stream.ToArray());
                Console.WriteLine(output);

    Now we’ll go through on each kind of payload.

    Write metadata

    Write metadata is simple, just use WriteMetadataDocument method in ODataMessageWriter.

     writer.WriteMetadataDocument();

    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.

    ODataMessageWriter writer = new ODataMessageWriter((IODataRequestMessage) message, settings, model);
                writer.WriteMetadataDocument();
    ODataMessageWriter writer = new ODataMessageWriter((IODataResponseMessage) message);
                writer.WriteMetadataDocument();

    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.

    ODataServiceDocument serviceDocument = new ODataServiceDocument();
                serviceDocument.EntitySets = new []
                {
                    new ODataEntitySetInfo
                    {
                        Name = "Customers",
                        Title = "Customers",
                        Url = new Uri("Customers", UriKind.Relative),
                    },
                    new ODataEntitySetInfo
                    {
                        Name = "Orders",
                        Title = "Orders",
                        Url = new Uri("Orders", UriKind.Relative),
                    },
                };
    
                serviceDocument.Singletons = new[]
                {
                    new ODataSingletonInfo
                    {
                        Name = "Company",
                        Title = "Company",
                        Url = new Uri("Company", UriKind.Relative),
                    },
                };
    
                serviceDocument.FunctionImports = new[]
                {
                    new ODataFunctionImportInfo
                    {
                        Name = "GetOutOfDateOrders",
                        Title = "GetOutOfDateOrders",
                        Url = new Uri("GetOutOfDateOrders", UriKind.Relative),
                    },
                };

    Then let’s call WriteServiceDocument method to write it.

    writer.WriteServiceDocument(serviceDocument);

    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.

    ODataMessageWriterSettings settings = new ODataMessageWriterSettings();
                settings.ODataUri = new ODataUri()
                {
                    ServiceRoot = new Uri("http://services.odata.org/V4/OData/OData.svc/")
                };
    
                ODataMessageWriter writer = new ODataMessageWriter((IODataResponseMessage) message, settings);
    writer.WriteServiceDocument(serviceDocument);
                

    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.

    ODataServiceDocument serviceDocument = model.GenerateServiceDocument();
                writer.WriteServiceDocument(serviceDocument);

    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.

    await writer.WriteServiceDocumentAsync(serviceDocument);

    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.

    ODataMessageWriterSettings settings = new ODataMessageWriterSettings();
                settings.ODataUri = new ODataUri()
                {
                    ServiceRoot = new Uri("http://services.odata.org/V4/OData/OData.svc/")
                };
    
                ODataMessageWriter writer = new ODataMessageWriter((IODataResponseMessage)message, settings, model);
    
                IEdmEntitySet entitySet = model.FindDeclaredEntitySet("Customers");
                ODataWriter odataWriter = writer.CreateODataFeedWriter(entitySet);
    
                ODataFeed feed = new ODataFeed();
                odataWriter.WriteStart(feed);
                odataWriter.WriteEnd();

    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.

    {"@odata.context":"http://services.odata.org/V4/OData/OData.svc/$metadata#Customers","value":[]}

    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.

    ODataMessageWriterSettings settings = new ODataMessageWriterSettings();
                settings.ODataUri = new ODataUri()
                {
                    ServiceRoot = new Uri("http://services.odata.org/V4/OData/OData.svc/")
                };
    
                ODataMessageWriter writer = new ODataMessageWriter((IODataResponseMessage)message, settings);
    
                ODataWriter odataWriter = writer.CreateODataFeedWriter();
    
                ODataFeed feed = new ODataFeed();
                
                feed.SetSerializationInfo(new ODataFeedAndEntrySerializationInfo()
                {
                    NavigationSourceName = "Customers",
                    NavigationSourceEntityTypeName = "Customer"
                });
                odataWriter.WriteStart(feed);
                odataWriter.WriteEnd();

    When writting feed, you can provide a next page, which is used in server driven paging.

    ODataFeed feed = new ODataFeed();
                feed.NextPageLink = new Uri("Customers?next", UriKind.Relative);
                odataWriter.WriteStart(feed);
                odataWriter.WriteEnd();

    The output will contains a next link before the value collection.

    {"@odata.context":"http://services.odata.org/V4/OData/OData.svc/$metadata#Customers","@odata.nextLink":"Customers?next","value":[]}

    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.

    ODataFeed feed = new ODataFeed();
                odataWriter.WriteStart(feed);
                feed.NextPageLink = new Uri("Customers?next", UriKind.Relative);
                odataWriter.WriteEnd();
    {"@odata.context":"http://services.odata.org/V4/OData/OData.svc/$metadata#Customers","value":[],"@odata.nextLink":"Customers?next"}

    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.

    ODataFeed feed = new ODataFeed();
                odataWriter.WriteStart(feed);
    
                ODataEntry entry = new ODataEntry()
                {
                    Properties = new[]
                    {
                        new ODataProperty()
                        {
                            Name = "Id",
                            Value = 1,
                        },
                        new ODataProperty()
                        {
                            Name = "Name",
                            Value = "Tom",
                        }
                    }
                };
    
                odataWriter.WriteStart(entry);
                odataWriter.WriteEnd();
                odataWriter.WriteEnd();
    {"@odata.context":"http://services.odata.org/V4/OData/OData.svc/$metadata#Customers","value":[{"Id":1,"Name":"Tom"}]}

    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.

    ODataMessageWriter writer = new ODataMessageWriter((IODataResponseMessage)message, settings, model);
    
                IEdmEntitySet entitySet = model.FindDeclaredEntitySet("Customers");
                ODataWriter odataWriter = writer.CreateODataEntryWriter(entitySet);
    
                ODataEntry entry = new ODataEntry()
                {
                    Properties = new[]
                    {
                        new ODataProperty()
                        {
                            Name = "Id",
                            Value = 1,
                        },
                        new ODataProperty()
                        {
                            Name = "Name",
                            Value = "Tom",
                        }
                    }
                };
    
                odataWriter.WriteStart(entry);
                odataWriter.WriteEnd();
    {"@odata.context":"http://services.odata.org/V4/OData/OData.svc/$metadata#Customers/$entity","Id":1,"Name":"Tom"}

    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.

    ODataMessageWriter writer = new ODataMessageWriter((IODataResponseMessage)message, settings, model);
    
                IEdmEntitySet entitySet = model.FindDeclaredEntitySet("Customers");
                ODataWriter odataWriter = writer.CreateODataEntryWriter(entitySet);
    
                ODataEntry entry = new ODataEntry()
                {
                    Properties = new[]
                    {
                        new ODataProperty()
                        {
                            Name = "Id",
                            Value = 1,
                        },
                        new ODataProperty()
                        {
                            Name = "Name",
                            Value = "Tom",
                        }
                    }
                };
    
                ODataEntry orderEntry = new ODataEntry()
                {
                    Properties = new[]
                    {
                        new ODataProperty()
                        {
                            Name = "Id",
                            Value = 1,
                        },
                        new ODataProperty()
                        {
                            Name = "Price",
                            Value = new decimal(3.14)
                        }
                    }
                };
    
                odataWriter.WriteStart(entry);
                odataWriter.WriteStart(new ODataNavigationLink()
                {
                    Name = "Purchases",
                    IsCollection = true
                });
                odataWriter.WriteStart(new ODataFeed());
                odataWriter.WriteStart(orderEntry);
                odataWriter.WriteEnd();
                odataWriter.WriteEnd();
                odataWriter.WriteEnd();
                odataWriter.WriteEnd();

    The output will contains order entity inside the customer entity.

    {"@odata.context":"http://services.odata.org/V4/OData/OData.svc/$metadata#Customers/$entity","Id":1,"Name":"Tom","Purchases":[{"Id":1,"Price":3.14}]}
  • 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.

    IEdmModel model = builder
                    .BuildAddressType()
                    .BuildCategoryType()
                    .BuildCustomerType()
                    .BuildDefaultContainer()
                    .BuildCustomerSet()
                    .GetModel();

    Then set up the message to read the payload.

    MemoryStream stream = new MemoryStream();
    InMemoryMessage message = new InMemoryMessage() {Stream = stream};

    Create the settings:

    ODataMessageReaderSettings settings = new ODataMessageReaderSettings();

    Now we are ready to create the ODataMessageReader instance:

    ODataMessageReader reader = new ODataMessageReader((IODataResponseMessage) message, settings);

    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.

    stream.Position = 0;

    Here is the whole program that use SampleModelBuilder and InMemoryMessage to first write then read metadata payload:

    IEdmModel model = builder
                    .BuildAddressType()
                    .BuildCategoryType()
                    .BuildOrderType()
                    .BuildCustomerType()
                    .BuildDefaultContainer()
                    .BuildOrderSet()
                    .BuildCustomerSet()
                    .GetModel();
    
                MemoryStream stream = new MemoryStream();
                InMemoryMessage message = new InMemoryMessage() { Stream = stream };
    
                ODataMessageWriterSettings writerSettings = new ODataMessageWriterSettings();
    
                ODataMessageWriter writer = new ODataMessageWriter((IODataResponseMessage)message, writerSettings, model);
    
                writer.WriteMetadataDocument();
                stream.Position = 0;
    
                ODataMessageReaderSettings settings = new ODataMessageReaderSettings();
                
                ODataMessageReader reader = new ODataMessageReader((IODataResponseMessage)message, settings);
    
                IEdmModel modelFromReader = reader.ReadMetadataDocument();

    Now we’ll go through on each kind of payload.

    Read metadata

    Read metadata is simple, just use ReadMetadataDocument method in ODataMessageReader.

     reader.ReadMetadataDocument();

    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.

    ODataMessageReaderSettings readerSettings = new ODataMessageReaderSettings();
    
                ODataMessageReader reader = new ODataMessageReader((IODataResponseMessage)message, readerSettings, model);
    
                ODataServiceDocument serviceDocumentFromReader = reader.ReadServiceDocument();

    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.

    ODataServiceDocument serviceDocument = await reader.ReadServiceDocumentAsync();

    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.

    ODataMessageReader reader = new ODataMessageReader((IODataResponseMessage)message, readerSettings, model);
                ODataReader feedReader = reader.CreateODataFeedReader(entitySet, entitySet.EntityType());
                while (feedReader.Read())
                {
                    switch (feedReader.State)
                    {
                        case ODataReaderState.FeedEnd:
                            ODataFeed feedFromReader = (ODataFeed)feedReader.Item;
                            break;
                        case ODataReaderState.EntryEnd:
                            ODataEntry entryFromReader = (ODataEntry)feedReader.Item;
                            break;
                    }
                }

    Read Entry

    To read a top level entry, use ODataMessageReader.CreateEntryReader. Other than that, there is no different compared to read feed.

    ODataMessageReader reader = new ODataMessageReader((IODataResponseMessage)message, readerSettings, model);
                ODataReader feedReader = reader.CreateODataEntryReader(entitySet, entitySet.EntityType());
                while (feedReader.Read())
                {
                    switch (feedReader.State)
                    {
                        case ODataReaderState.FeedEnd:
                            ODataFeed feedFromReader = (ODataFeed)feedReader.Item;
                            break;
                        case ODataReaderState.EntryEnd:
                            ODataEntry entryFromReader = (ODataEntry)feedReader.Item;
                            break;
                    }
                }
  • 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:

    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:

    public ODataUriParser(IEdmModel model, Uri serviceRoot, Uri fullUri);

    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.

    Uri serviceRoot = new Uri("http://services.odata.org/V4/OData/OData.svc");
    IEdmModel model = EdmxReader.Parse(XmlReader.Create(serviceRoot + "/$metadata"));
    Uri fullUri = new Uri("http://services.odata.org/V4/OData/OData.svc/Products");
    ODataUriParser parser = new ODataUriParser(model, serviceRoot, fullUri);

    Parsing Resource Path

    You can use the following API to parse resource path:

    Uri fullUri = new Uri("http://services.odata.org/V4/OData/OData.svc/Products(1)");
    ODataUriParser parser = new ODataUriParser(model, serviceRoot, fullUri);
    ODataPath path = parser.ParsePath();

    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) :

    Uri fullUri = new Uri("Products?$select=ID&$expand=ProductDetail" +
        "&$filter=Categories/any(d:d/ID%20gt%201)&$orderby=ID%20desc" +
        "&$top=1&$count=true&$search=tom", UriKind.Relative);
    ODataUriParser parser = new ODataUriParser(model, serviceRoot, fullUri);
    SelectExpandClause expand = 
        parser.ParseSelectAndExpand();              //parse $select, $expand
    FilterClause filter = parser.ParseFilter();     // parse $filter
    OrderByClause orderby = parser.ParseOrderBy();  // parse $orderby
    SearchClause search = parser.ParseSearch();     // parse $search
    long? top = parser.ParseTop();                  // parse $top
    long? skip = parser.ParseSkip();                // parse $skip
    bool? count = parser.ParseCount();              // parse $count

    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:

    SearchQueryOption
        Expression = BinaryOperatorNode
                       OperationKind = BinaryOperatorKind.And
                       Left           = SearchTermNode
                                        Text = a
                       Right          = SearchTermNode
                                        Text = b

    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:

    public ODataQueryOptionParser(IEdmModel model, IEdmType targetEdmType, IEdmNavigationSource targetNavigationSource, IDictionary<string, string> queryOptions);

    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:

    Dictionary<string, string> options = new Dictionary<string, string>()
    {
        {"$select"  , "ID"                          },
        {"$expand"  , "ProductDetail"               },
        {"$filter"  , "Categories/any(d:d/ID gt 1)" },
        {"$orderby" , "ID desc"                     },
        {"$top"     , "1"                           },
        {"$count"   , "true"                        },
        {"$search"  , "tom"                         },
    };
     
    IEdmType type = model.FindDeclaredType("ODataDemo.Product");
    IEdmNavigationSource source = model.FindDeclaredEntitySet("Products");
    ODataQueryOptionParser parser 
        = new ODataQueryOptionParser(model, type, source, options);
    SelectExpandClause selectExpand = 
        parser.ParseSelectAndExpand();              //parse $select, $expand
    FilterClause filter = parser.ParseFilter();     // parse $filter
    OrderByClause orderby = parser.ParseOrderBy();  // parse $orderby
    SearchClause search = parser.ParseSearch();     // parse $search
    long? top = parser.ParseTop();                  // parse $top
    long? skip = parser.ParseSkip();                // parse $skip (null)
    bool? count = parser.ParseCount();              // parse $count

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:

    Install-Package Microsoft.OData.Edm

    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:

    using Microsoft.OData.Edm;
    using Microsoft.OData.Edm.Library;

    Then replace the boilerplate code with the following:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            private readonly EdmModel _model = new EdmModel();
            public IEdmModel GetModel()
            {
                return _model;
            }
        }
    }

    Add a Complex Type Address

    In the SampleModelBuilder.cs file, add the following code into the SampleModelBuilder class:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            private EdmComplexType _addressType;
            ...
            public SampleModelBuilder BuildAddressType()
            {
                _addressType = new EdmComplexType("Sample.NS", "Address");
                _addressType.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String);
                _addressType.AddStructuralProperty("City", EdmPrimitiveTypeKind.String);
                _addressType.AddStructuralProperty("PostalCode", EdmPrimitiveTypeKind.Int32);
                _model.AddElement(_addressType);
                return this;
            }
            ...
        }
    }

    This code:

    • Defines a keyless complex type Address within the namespace Sample.NS;
    • Adds three structural properties Street, City and PostalCode;
    • 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:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            private EdmEnumType _categoryType;
            ...
            public SampleModelBuilder BuildCategoryType()
            {
                _categoryType = new EdmEnumType("Sample.NS", "Category", EdmPrimitiveTypeKind.Int64, isFlags: true);
                _categoryType.AddMember("Books", new EdmIntegerConstant(1L));
                _categoryType.AddMember("Dresses", new EdmIntegerConstant(2L));
                _categoryType.AddMember("Sports", new EdmIntegerConstant(4L));
                _model.AddElement(_categoryType);
                return this;
            }
            ...
        }
    }

    This code:

    • Defines an enumeration type Category based on Edm.Int64 within the namespace Sample.NS;
    • Sets the attribute IsFlags to true so multiple members can be selected simultaneously;
    • Adds three enumeration members Books, Dresses and Sports;
    • 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:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            private EdmEntityType _customerType;
            ...
            public SampleModelBuilder BuildCustomerType()
            {
                _customerType = new EdmEntityType("Sample.NS", "Customer");
                _customerType.AddKeys(_customerType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32, isNullable: false));
                _customerType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String, isNullable: false);
                _customerType.AddStructuralProperty("Credits",
                    new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetInt64(isNullable: true))));
                _customerType.AddStructuralProperty("Interests", new EdmEnumTypeReference(_categoryType, isNullable: true));
                _customerType.AddStructuralProperty("Address", new EdmComplexTypeReference(_addressType, isNullable: false));
                _model.AddElement(_customerType);
                return this;
            }
            ...
        }
    }

    This code:

    • Defines an entity type Customer within the namespace Sample.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 type Collection(Edm.Int64);
    • Adds a nullable property Interests of the type Sample.NS.Category;
    • Adds a non-nullable property Address of the type Sample.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:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            private EdmEntityContainer _defaultContainer;
            ...
            public SampleModelBuilder BuildDefaultContainer()
            {
                _defaultContainer = new EdmEntityContainer("Sample.NS", "DefaultContainer");
                _model.AddElement(_defaultContainer);
                return this;
            }
            ...
        }
    }

    This code:

    • Defines an entity container DefaultContainer of the namespace Sample.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:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            private EdmEntitySet _customerSet;
            ...
            public SampleModelBuilder BuildCustomerSet()
            {
                _customerSet = _defaultContainer.AddEntitySet("Customers", _customerType);
                return this;
            }
            ...
        }
    }

    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:

    using System.Collections.Generic;
    using System.IO;
    using System.Xml;
    using Microsoft.OData.Edm;
    using Microsoft.OData.Edm.Csdl;
    using Microsoft.OData.Edm.Validation;

    Then replace the boilerplate Program class with the following:

    namespace EdmLibSample
    {
        class Program
        {
            public static void Main(string[] args)
            {
                var builder = new SampleModelBuilder();
                var model = builder
                    .BuildAddressType()
                    .BuildCategoryType()
                    .BuildCustomerType()
                    .BuildDefaultContainer()
                    .BuildCustomerSet()
                    .GetModel();
                WriteModelToCsdl(model, "csdl.xml");
            }
            private static void WriteModelToCsdl(IEdmModel model, string fileName)
            {
                using (var writer = XmlWriter.Create(fileName))
                {
                    IEnumerable<EdmError> errors;
                    model.TryWriteCsdl(writer, out errors);
                }
            }
        }
    }

    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

    [Tutorial & Sample] Use Enumeration types in OData.

  • 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 the CsdlReader 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.

    namespace EdmLibSample
    {
        class Program
        {
            ...
            private static void WriteModelToCsdl(IEdmModel model, string fileName)
            {
                using (var writer = XmlWriter.Create(fileName))
                {
                    IEnumerable<EdmError> errors;
                    model.TryWriteCsdl(writer, out errors);
                }
            }
        }
    }

    The CsdlWriter.TryWriteCsdl() method is defined as an extension method to IEdmModel:

    namespace Microsoft.OData.Edm.Csdl
    {
        public static class CsdlWriter
        {
            ...
            public static bool TryWriteCsdl(this IEdmModel model, XmlWriter writer, out IEnumerable<EdmError> errors);
            ...
        }
    }

    The second parameter writer requires an XmlWriter which can be created through the overloaded XmlWriter.Create methods. Remember to either apply a using clause to an XmlWriter instance or explicitly call XmlWriter.Flush() (or XmlWriter.Close()) to flush the buffer to its underlying stream. The third parameter errors is used to pass out the errors found when writing the model. If the method returns true (indicating write success), the errors should be an empty Enumerable; otherwise it contains all the model errors.

    The other version of the CsdlWriter.TryWriteCsdl() method is:

    namespace Microsoft.OData.Edm.Csdl
    {
        public static class CsdlWriter
        {
            ...
            public static bool TryWriteCsdl(this IEdmModel model, Func<string, XmlWriter> writerProvider, out IEnumerable<EdmError> errors);
            ...
        }
    }

    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 different XmlWriter for each referenced model where the string parameter is the schema namespace of that model. A simple writerProvider would be:

    public XmlWriter CreateXmlWriter(string namespace)
    {
        return XmlWriter.Create(string.Format("{0}.xml", namespace));
    }

    Using the CsdlReader APIs

    The CsdlReader APIs are defined as follows:

    namespace Microsoft.OData.Edm.Csdl
    {
        public static class CsdlReader
        {
            public static bool TryParse(IEnumerable<XmlReader> readers, out IEdmModel model, out IEnumerable<EdmError> errors);
            public static bool TryParse(IEnumerable<XmlReader> readers, IEdmModel reference, out IEdmModel model, out IEnumerable<EdmError> errors);
            public static bool TryParse(IEnumerable<XmlReader> readers, IEnumerable<IEdmModel> references, out IEdmModel model, out IEnumerable<EdmError> errors);
        }
    }

    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 of XmlReader each of which reads a CSDL document. The second paramter model passes out the parsed model. The third parameter errors passes out the errors when parsing the CSDL document. If the return value of this method is true (indicating parse success), the errors 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:

    namespace EdmLibSample
    {
        class Program
        {
            public static void Main(string[] args)
            {
                ...
                WriteModelToCsdl(model, "csdl.xml");
    #region     !!!INSERT THE CODE BELOW!!!
                var model1 = ReadModel("csdl.xml");
                WriteModelToCsdl(model1, "csdl1.xml");
    #endregion
            }
            ...
            private static IEdmModel ReadModel(string fileName)
            {
                using (var reader = XmlReader.Create(fileName))
                {
                    IEdmModel model;
                    IEnumerable<EdmError> errors;
                    if (CsdlReader.TryParse(new[] { reader }, out model, out errors))
                    {
                        return model;
                    }
                    return null;
                }
            }
        }
    }

    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

    [Tutorial & Sample] Refering when Constructing EDM Model.

  • 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:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            private EdmNavigationProperty _friendsProperty;
            ...
            public SampleModelBuilder BuildCustomerType()
            {
                ...
                _customerType.AddStructuralProperty("Address", new EdmComplexTypeReference(_addressType, isNullable: false));
    #region     !!!INSERT THE CODE BELOW!!!
                _friendsProperty = _customerType.AddUnidirectionalNavigation(
                    new EdmNavigationPropertyInfo
                    {
                        ContainsTarget = false,
                        Name = "Friends",
                        Target = _customerType,
                        TargetMultiplicity = EdmMultiplicity.Many
                    });
    #endregion
                _model.AddElement(_customerType);
                return this;
            }
            ...
            public SampleModelBuilder BuildCustomerSet()
            {
                _customerSet = _defaultContainer.AddEntitySet("Customers", _customerType);
    #region     !!!INSERT THE CODE BELOW!!!
                _customerSet.AddNavigationTarget(_friendsProperty, _customerSet);
    #endregion
                return this;
            }
            ...
        }
    }

    This code:

    • Adds a navigation property Friends to the entity type Customer;
    • Sets the ContainsTarget property to false since this property has no contained entities and targets one or more Customer entites in the entity set Customers;
    • Sets the TargetMultiplicity property to EdmMultiplicity.Many indicating that one customer can have many orders. Other possible values are ZeroOrOne and One;

    Add an Entity Type Order and an Entity Set Orders

    Just as how we added the entity set Customers, we first add an entity type Order and then the entity set Orders.

    In the SampleModelBuilder.cs file, insert the following code into the SampleModelBuilder class:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            private EdmEntityType _orderType;
            ...
            private EdmEntitySet _orderSet;
            ...
            public SampleModelBuilder BuildOrderType()
            {
                _orderType = new EdmEntityType("Sample.NS", "Order");
                _orderType.AddKeys(_orderType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32));
                _orderType.AddStructuralProperty("Price", EdmPrimitiveTypeKind.Decimal);
                _model.AddElement(_orderType);
                return this;
            }
            ...
            public SampleModelBuilder BuildOrderSet()
            {
                _orderSet = _defaultContainer.AddEntitySet("Orders", _orderType);
                return this;
            }
            ...
        }
    }

    In the Program.cs file, insert the following code into the Main method:

    namespace EdmLibSample
    {
        class Program
        {
            public static void Main(string[] args)
            {
                var builder = new SampleModelBuilder();
                var model = builder
                    .BuildAddressType()
                    .BuildCategoryType()
    #region         !!!INSERT THE CODE BELOW!!!
                    .BuildOrderType()
    #endregion
                    .BuildCustomerType()
                    .BuildDefaultContainer()
    #region         !!!INSERT THE CODE BELOW!!!
                    .BuildOrderSet()
    #endregion
                    .BuildCustomerSet()
                    .GetModel();
                WriteModelToCsdl(model, "csdl.xml");
            }
        }
    }

    Add Navigation Properties Purchases and Intentions

    In the SampleModelBuilder.cs file, insert the following code into the SampleModelBuilder class:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            private EdmNavigationProperty _purchasesProperty;
            private EdmNavigationProperty _intentionsProperty;
            ...
            public SampleModelBuilder BuildCustomerType()
            {
                ...
    #region     !!!INSERT THE CODE BELOW!!!
                _purchasesProperty = _customerType.AddUnidirectionalNavigation(
                    new EdmNavigationPropertyInfo
                    {
                        ContainsTarget = false,
                        Name = "Purchases",
                        Target = _orderType,
                        TargetMultiplicity = EdmMultiplicity.Many
                    });
                _intentionsProperty = _customerType.AddUnidirectionalNavigation(
                    new EdmNavigationPropertyInfo
                    {
                        ContainsTarget = true,
                        Name = "Intentions",
                        Target = _orderType,
                        TargetMultiplicity = EdmMultiplicity.Many
                    });
    #endregion
                _model.AddElement(_customerType);
                return this;
            }
            ...
            public SampleModelBuilder BuildCustomerSet()
            {
                _customerSet = _defaultContainer.AddEntitySet("Customers", _customerType);
                _customerSet.AddNavigationTarget(_friendsProperty, _customerSet);
    #region     !!!INSERT THE CODE BELOW!!!
                _customerSet.AddNavigationTarget(_purchasesProperty, _orderSet);
    #endregion
                return this;
            }
        }
    }

    This code:

    • Adds a Purchases property targetting one or more settled orders in the entity set Orders;
    • Adds a Intentions property targetting a contained entity set of unsettled orders that should not be listed in the entity set Orders.

    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] Containment is Coming with OData V4.

  • 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:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            private EdmSingleton _vipCustomer;
            ...
            public SampleModelBuilder BuildVipCustomer()
            {
                _vipCustomer = _defaultContainer.AddSingleton("VipCustomer", _customerType);
                return this;
            }
            ...
        }
    }

    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:

    namespace EdmLibSample
    {
        class Program
        {
            public static void Main(string[] args)
            {
                var builder = new SampleModelBuilder();
                var model = builder
                    ...
                    .BuildCustomerSet()
    #region         !!!INSERT THE CODE BELOW!!!
                    .BuildVipCustomer()
    #endregion
                    .GetModel();
                WriteModelToCsdl(model, "csdl.xml");
            }
        }
    }

    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:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            private EdmEntityType _urgentOrderType;
            ...
            public SampleModelBuilder BuildUrgentOrderType()
            {
                _urgentOrderType = new EdmEntityType("Sample.NS", "UrgentOrder", _orderType);
                _urgentOrderType.AddStructuralProperty("Deadline", EdmPrimitiveTypeKind.Date);
                _model.AddElement(_urgentOrderType);
                return this;
            }
            ...
        }
    }

    Then in the Program.cs file, insert the following code into the Main method:

    namespace EdmLibSample
    {
        class Program
        {
            public static void Main(string[] args)
            {
                var builder = new SampleModelBuilder();
                var model = builder
                    ...
                    .BuildOrderType()
    #region         !!!INSERT THE CODE BELOW!!!
                    .BuildUrgentOrderType()
    #endregion
                    ...
                    .GetModel();
                WriteModelToCsdl(model, "csdl.xml");
            }
        }
    }

    This code:

    • Defines a derived entity type UrgentOrder within the namespace Sample.NS whose base type is Sample.NS.Order;
    • Adds a structural property Deadline of type Edm.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:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            private EdmComplexType _workAddressType;
            ...
            public SampleModelBuilder BuildWorkAddressType()
            {
                _workAddressType = new EdmComplexType("Sample.NS", "WorkAddress", _addressType);
                _workAddressType.AddStructuralProperty("Company", EdmPrimitiveTypeKind.String);
                _model.AddElement(_workAddressType);
                return this;
            }
            ...
        }
    }

    Then in the Program.cs file, insert the following code into the Main method:

    namespace EdmLibSample
    {
        class Program
        {
            public static void Main(string[] args)
            {
                var builder = new SampleModelBuilder();
                var model = builder
                    .BuildAddressType()
    #region         !!!INSERT THE CODE BELOW!!!
                    .BuildWorkAddressType()
    #endregion
                    ...
                    .GetModel();
                WriteModelToCsdl(model, "csdl.xml");
            }
        }
    }

    This code:

    • Defines a derived complex type WorkAddress within the namespace Sample.NS whose base type is Sample.NS.Address;
    • Adds a structural property Company of type Edm.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:

  • 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:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            private EdmAction _rateAction;
            ...
            public SampleModelBuilder BuildRateAction()
            {
                _rateAction = new EdmAction("Sample.NS", "Rate",
                    returnType: null, isBound: true, entitySetPathExpression: null);
                _rateAction.AddParameter("customer", new EdmEntityTypeReference(_customerType, false));
                _rateAction.AddParameter("rating", EdmCoreModel.Instance.GetInt32(false));
                _model.AddElement(_rateAction);
                return this;
            }
            ...
        }
    }

    Then in the Program.cs file, insert the following code into the Main method:

    namespace EdmLibSample
    {
        class Program
        {
            public static void Main(string[] args)
            {
                var builder = new SampleModelBuilder();
                var model = builder
                    ...
                    .BuildCustomerType()
    #region         !!!INSERT THE CODE BELOW!!!
                    .BuildRateAction()
    #endregion
                    ...
                    .GetModel();
                WriteModelToCsdl(model, "csdl.xml");
            }
        }
    }

    This code:

    • Defines a bound action Rate within the namespace Sample.NS with no return type;
    • Adds a binding parameter customer of type Sample.NS.Customer;
    • Adds a parameter rating of type Edm.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:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            private EdmFunction _mostExpensiveFunction;
            ...
            public SampleModelBuilder BuildMostExpensiveFunction()
            {
                _mostExpensiveFunction = new EdmFunction("Sample.NS", "MostExpensive",
                    new EdmEntityTypeReference(_orderType, true), isBound: false, entitySetPathExpression: null, isComposable: true);
                _model.AddElement(_mostExpensiveFunction);
                return this;
            }
            ...
        }
    }

    Then in the Program.cs file, insert the following code into the Main method:

    namespace EdmLibSample
    {
        class Program
        {
            public static void Main(string[] args)
            {
                var builder = new SampleModelBuilder();
                var model = builder
                    ...
                    .BuildRateAction()
    #region         !!!INSERT THE CODE BELOW!!!
                    .BuildMostExpensiveFunction()
    #endregion
                    ...
                    .GetModel();
                WriteModelToCsdl(model, "csdl.xml");
            }
        }
    }

    This code:

    • Defines an unbound composable function MostExpensive within the namespace Sample.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:

    using Microsoft.OData.Edm.Library.Expressions;

    Then add the following code into the SampleModelBuilder class:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            private EdmFunctionImport _mostValuableFunctionImport;
            ...
            public SampleModelBuilder BuildMostValuableFunctionImport()
            {
                _mostValuableFunctionImport = _defaultContainer.AddFunctionImport("MostValuable", _mostExpensiveFunction, new EdmEntitySetReferenceExpression(_orderSet));
                return this;
            }
            ...
        }
    }

    And in the Program.cs file, insert the following code into the Main method:

    namespace EdmLibSample
    {
        class Program
        {
            public static void Main(string[] args)
            {
                var builder = new SampleModelBuilder();
                var model = builder
                    ...
                    .BuildVipCustomer()
    #region         !!!INSERT THE CODE BELOW!!!
                    .BuildMostValuableFunctionImport()
    #endregion
                    .GetModel();
                WriteModelToCsdl(model, "csdl.xml");
            }
        }
    }

    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 set Orders;

    The Sample.NS.MostValuable function import is actually the Sample.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:

  • 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:

    using Microsoft.OData.Edm.Csdl;
    using Microsoft.OData.Edm.Library.Annotations;
    using Microsoft.OData.Edm.Values;

    Then add the following code into the SampleModelBuilder class:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            public SampleModelBuilder BuildAnnotations()
            {
                var term1 = new EdmTerm("Sample.NS", "MaxCount", EdmCoreModel.Instance.GetInt32(true));
                var annotation1 = new EdmAnnotation(_customerSet, term1, new EdmIntegerConstant(10000000L));
                _model.AddVocabularyAnnotation(annotation1);
                return this;
            }
            ...
        }
    }

    And in the Program.cs file, insert the following code into the Main method:

    namespace EdmLibSample
    {
        class Program
        {
            public static void Main(string[] args)
            {
                var builder = new SampleModelBuilder();
                var model = builder
                    ...
                    .BuildMostValuableFunctionImport()
    #region         !!!INSERT THE CODE BELOW!!!
                    .BuildAnnotations()
    #endregion
                    .GetModel();
                WriteModelToCsdl(model, "csdl.xml");
            }
        }
    }

    This code adds an Edm.Int32 annotation Sample.NS.MaxCount targetting the entity set Customers to the Annotations element.

    Add an Inline Annotation to the Entity Type Customer

    In the SampleModelBuilder.cs file, insert the following code into the SampleModelBuilder.BuildAnnotations() method:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            public SampleModelBuilder BuildAnnotations()
            {
                ...
                _model.AddVocabularyAnnotation(annotation1);
    #region     !!!INSERT THE CODE BELOW!!!
                var term2 = new EdmTerm("Sample.NS", "KeyName", EdmCoreModel.Instance.GetString(true));
                var annotation2 = new EdmAnnotation(_customerType, term2, new EdmStringConstant("Id"));
                annotation2.SetSerializationLocation(_model, EdmVocabularyAnnotationSerializationLocation.Inline);
                _model.AddVocabularyAnnotation(annotation2);
    #endregion
                return this;
            }
            ...
        }
    }

    This code adds an inline Edm.String annotation Sample.NS.KeyName targetting the entity type Customer.

    Add an Inline Annotation to the Property Customer.Name

    In the SampleModelBuilder.cs file, insert the following code into the SampleModelBuilder.BuildAnnotations() method:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            public SampleModelBuilder BuildAnnotations()
            {
                ...
                _model.AddVocabularyAnnotation(annotation2);
    #region     !!!INSERT THE CODE BELOW!!!
                var term3 = new EdmTerm("Sample.NS", "Width", EdmCoreModel.Instance.GetInt32(true));
                var annotation3 = new EdmAnnotation(_customerType.FindProperty("Name"), term3, new EdmIntegerConstant(10L));
                annotation3.SetSerializationLocation(_model, EdmVocabularyAnnotationSerializationLocation.Inline);
                _model.AddVocabularyAnnotation(annotation3);
    #endregion
                return this;
            }
            ...
        }
    }

    This code adds an inline Edm.Int32 annotation Sample.NS.Width targetting the property Customer.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:

    namespace EdmLibSample
    {
        class Program
        {
            public static void Main(string[] args)
            {
                ...
                WriteModelToCsdl(model, "csdl.xml");
    #region     !!!INSERT THE CODE BELOW!!!
                TestExtensionMethods(model);
    #endregion
                var model1 = ReadModel("csdl.xml");
                ...
            }
            ...
            private static void TestExtensionMethods(IEdmModel model)
            {
                // Find an entity set.
                var customerSet = model.FindDeclaredEntitySet("Customers");
                Console.WriteLine("{0} '{1}' found.", customerSet.NavigationSourceKind(), customerSet.Name);
                // Find any kind of navigation source (entity set or singleton).
                var vipCustomer = model.FindDeclaredNavigationSource("VipCustomer");
                Console.WriteLine("{0} '{1}' found.", vipCustomer.NavigationSourceKind(), vipCustomer.Name);
                // Find a type (complex or entity or enum).
                var orderType = model.FindDeclaredType("Sample.NS.Order");
                Console.WriteLine("{0} type '{1}' found.", orderType.TypeKind, orderType.FullName());
                var addressType = model.FindDeclaredType("Sample.NS.Address");
                Console.WriteLine("{0} type '{1}' found.", addressType.TypeKind, addressType);
                // Find derived type of some type.
                var workAddressType = model.FindAllDerivedTypes((IEdmStructuredType)addressType).Single();
                Console.WriteLine("Type '{0}' is the derived from '{1}'.", ((IEdmSchemaType)workAddressType).Name, addressType.Name);
                // Find an operation.
                var rateAction = model.FindDeclaredOperations("Sample.NS.Rate").Single();
                Console.WriteLine("{0} '{1}' found.", rateAction.SchemaElementKind, rateAction.Name);
                // Find an operation import.
                var mostValuableFunctionImport = model.FindDeclaredOperationImports("MostValuable").Single();
                Console.WriteLine("{0} '{1}' found.", mostValuableFunctionImport.ContainerElementKind, mostValuableFunctionImport.Name);
                // Find an annotation and get its value.
                var maxCountAnnotation = (IEdmValueAnnotation)model.FindDeclaredVocabularyAnnotations(customerSet).Single();
                var maxCountValue = ((IEdmIntegerValue)maxCountAnnotation.Value).Value;
                Console.WriteLine("'{0}' = '{1}' on '{2}'", maxCountAnnotation.Term.Name, maxCountValue, ((IEdmEntitySet)maxCountAnnotation.Target).Name);
            }
        }
    }

    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:

  • 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 type NS1.Complex1 which will have a structural property of another complex type defined in another model. We also add an EDM reference to subModel1 pointing to the second model located at http://model2. This URL should be the service metadata location. The namespace to include is NS2 and the model alias is Alias2.

    var subModel1 = new EdmModel();
    
    var complex1 = new EdmComplexType("NS1", "Complex1");
    subModel1.AddElement(complex1);
    
    var reference1 = new EdmReference("http://model2");
    reference1.AddInclude(new EdmInclude("Alias2", "NS2"));
    
    var references1 = new List<IEdmReference> {reference1};
    subModel1.SetEdmReferences(references1);

    Then we do the same thing for the second sub model subModel2. This model contains a complex type NS2.Complex2 and references the first model located at http://model1.

    var subModel2 = new EdmModel();
    
    var complex2 = new EdmComplexType("NS2", "Complex2");
    subModel2.AddElement(complex2);
    
    var reference2 = new EdmReference("http://model1");
    reference2.AddInclude(new EdmInclude("Alias1", "NS1"));
    
    var references2 = new List<IEdmReference> {reference2};
    subModel2.SetEdmReferences(references2);

    Now we will add one structural property to the two complex types NS1.Complex1 and NS2.Complex2 respectively. The key is that the property type is defined in the other model.

    complex1.AddStructuralProperty("Prop", new EdmComplexTypeReference(complex2, true));
    complex2.AddStructuralProperty("Prop", new EdmComplexTypeReference(complex1, true));

    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 type NS1.Complex1 and NS2.Complex2 respectively.

    var mainModel = new EdmModel();
    
    var complex3 = new EdmComplexType("NS", "Complex3");
    complex3.AddStructuralProperty("Prop1", new EdmComplexTypeReference(complex1, true));
    complex3.AddStructuralProperty("Prop2", new EdmComplexTypeReference(complex2, true));
    mainModel.AddElement(complex3);
    
    var references3 = new List<IEdmReference> { reference1, reference2 };
    mainModel.SetEdmReferences(references3);

    Define Model References by EDMX

    As an example, we store the EDMX of the three models in three string constants and create three StringReaders as if we are reading the model contents from remote locations.

    const string mainEdmx =
    @"<?xml version=""1.0"" encoding=""utf-16""?>
    <edmx:Edmx Version=""4.0"" xmlns:edmx=""http://docs.oasis-open.org/odata/ns/edmx"">
      <edmx:Reference Uri=""http://model2"">
        <edmx:Include Namespace=""NS2"" Alias=""Alias2"" />
      </edmx:Reference>
      <edmx:Reference Uri=""http://model1"">
        <edmx:Include Namespace=""NS1"" Alias=""Alias1"" />
      </edmx:Reference>
      <edmx:DataServices>
        <Schema Namespace=""NS"" xmlns=""http://docs.oasis-open.org/odata/ns/edm"">
          <ComplexType Name=""Complex3"">
            <Property Name=""Prop1"" Type=""NS1.Complex1"" />
            <Property Name=""Prop2"" Type=""NS2.Complex2"" />
          </ComplexType>
        </Schema>
      </edmx:DataServices>
    </edmx:Edmx>";
    
    const string edmx1 =
    @"<?xml version=""1.0"" encoding=""utf-16""?>
    <edmx:Edmx Version=""4.0"" xmlns:edmx=""http://docs.oasis-open.org/odata/ns/edmx"">
      <edmx:Reference Uri=""http://model2"">
        <edmx:Include Namespace=""NS2"" Alias=""Alias2"" />
      </edmx:Reference>
      <edmx:DataServices>
        <Schema Namespace=""NS1"" xmlns=""http://docs.oasis-open.org/odata/ns/edm"">
          <ComplexType Name=""Complex1"">
            <Property Name=""Prop"" Type=""NS2.Complex2"" />
          </ComplexType>
        </Schema>
      </edmx:DataServices>
    </edmx:Edmx>";
    
    const string edmx2 =
    @"<?xml version=""1.0"" encoding=""utf-16""?>
    <edmx:Edmx Version=""4.0"" xmlns:edmx=""http://docs.oasis-open.org/odata/ns/edmx"">
      <edmx:Reference Uri=""http://model1"">
        <edmx:Include Namespace=""NS1"" Alias=""Alias1"" />
      </edmx:Reference>
      <edmx:DataServices>
        <Schema Namespace=""NS2"" xmlns=""http://docs.oasis-open.org/odata/ns/edm"">
          <ComplexType Name=""Complex2"">
            <Property Name=""Prop"" Type=""NS1.Complex1"" />
          </ComplexType>
        </Schema>
      </edmx:DataServices>
    </edmx:Edmx>";
    
    IEdmModel model;
    IEnumerable<EdmError> errors;
    if (!EdmxReader.TryParse(XmlReader.Create(new StringReader(mainEdmx)), (uri) =>
    {
        if (string.Equals(uri.AbsoluteUri, "http://model1/"))
        {
            return XmlReader.Create(new StringReader(edmx1));
        }
    
        if (string.Equals(uri.AbsoluteUri, "http://model2/"))
        {
            return XmlReader.Create(new StringReader(edmx2));
        }
    
        throw new Exception("invalid url");
    }, out model, out errors))
    {
        throw new Exception("bad model");
    }

    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

  • 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 property id of Edm.String.

    var model = new EdmModel();
    
    var customer = new EdmEntityType("Test", "Customer", null, false, true);
    var customerId = customer.AddStructuralProperty("id", EdmPrimitiveTypeKind.String, false);
    customer.AddKeys(customerId);
    model.AddElement(customer);

    Create an entity type Test.Order with a composite key consisting of two key properties customerId and orderId both of Edm.String.

    var order = new EdmEntityType("Test", "Order", null, false, true);
    var orderCustomerId = order.AddStructuralProperty("customerId", EdmPrimitiveTypeKind.String, true);
    var orderOrderId = order.AddStructuralProperty("orderId", EdmPrimitiveTypeKind.String, true);
    order.AddKeys(orderCustomerId, orderOrderId);
    model.AddElement(order);

    Customer.id is the principal property while Order.customerId is the dependent property. Create a navigation property orders on the principal entity type customer.

    var customerOrders = customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo
    {
        ContainsTarget = true,
        Name = "orders",
        Target = order,
        TargetMultiplicity = EdmMultiplicity.Many
    });

    Then, create its corresponding partner navigation property on the dependent entity type order with referential constraint.

    var orderCustomer = order.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo
    {
        ContainsTarget = false,
        Name = "customer",
        Target = customer,
        TargetMultiplicity = EdmMultiplicity.One,
        DependentProperties = new[] { orderCustomerId },
        PrincipalProperties = new[] { customerId }
    });

    Create an entity type Test.Detail with a composite key consisting of three key properties customerId of Edm.String, orderId of Edm.String and id of Edm.Int32.

    var detail = new EdmEntityType("Test", "Detail");
    var detailCustomerId = detail.AddStructuralProperty("customerId", EdmPrimitiveTypeKind.String);
    var detailOrderId = detail.AddStructuralProperty("orderId", EdmPrimitiveTypeKind.String);
    detail.AddKeys(detailCustomerId, detailOrderId, detail.AddStructuralProperty("id", EdmPrimitiveTypeKind.Int32, false));
    model.AddElement(detail);

    Create an entity type Test.DetailedOrder which is a derived type of Test.Order. We will use this type to illustrate type casting in between multiple navigation properties.

    var detailedOrder = new EdmEntityType("Test", "DetailedOrder", order);

    Come back to the type Test.Detail. There are two referential constraints here:

    • DetailedOrder.orderId is the principal property while Detail.orderId is the dependent property.
    • DetailedOrder.customerId is the principal property while Detail.customerId is the dependent property.

    Create a navigation property details.

    var detailedOrderDetails = detailedOrder.AddUnidirectionalNavigation(
        new EdmNavigationPropertyInfo
        {
            ContainsTarget = true,
            Name = "details"
            Target = detail,
            TargetMultiplicity = EdmMultiplicity.Many
        });
    model.AddElement(detailedOrder);

    Then, create its corresponding partner navigation property on the dependent entity type detail with referential constraint.

    var detailDetailedOrder = detail.AddUnidirectionalNavigation(
        new EdmNavigationPropertyInfo
        {
            ContainsTarget = false,
            Name = "detailedOrder"
            Target = detailedOrder,
            TargetMultiplicity = EdmMultiplicity.One,
            DependentProperties = new[] { detailOrderId, detailCustomerId },
            PrincipalProperties = new[] { orderOrderId, orderCustomerId }
        });

    Please note that you should NOT specify Customer.id as the principal property because the association (represented by the navigation property details) is from DetailedOrder to Detail rather than from Customer to Detail. And those properties must be specified in the same order.

    Then you can query the details either by a full key predicate

    http://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:

    namespace EdmLibSample
    {
        public class SampleModelBuilder
        {
            ...
            public SampleModelBuilder BuildAddressType()
            {
                ...
                _addressType.AddStructuralProperty("Postcode", EdmPrimitiveTypeKind.Int32);
    #region     !!!INSERT THE CODE BELOW!!!
                _addressType.AddStructuralProperty("GeometryLoc", EdmPrimitiveTypeKind.GeometryPoint);
                _addressType.AddStructuralProperty("GeographyLoc", new EdmSpatialTypeReference(EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.GeographyPoint), true, 1234));
    #endregion
                _model.AddElement(_addressType);
                return this;
            }
            ...
        }
    }

    This code:

    • Adds a default Edm.GeometryPoint property GeometryLoc to the Address type;
    • Adds an Edm.GeographyPoint property GeographyLoc with a type facet Srid=1234 to the Address type.

    Run the Sample

    Build and run the sample. Then open the csdl.xml file under the output directory. The content of csdl.xml should look like the following:

  • 3.2 Create spatial instances

    This section shows how to create spatial instances using Spatial APIs and return them as property values of OData entries.

    Create GeometryPoint and GeographyPoint instances

    In order to use spatial types, please add the following using directive:

    using Microsoft.Spatial;

    The following code shows how to create GeometryPoint and GeographyPoint instances:

    // Create a 2D GeometryPoint.
    GeometryPoint point1 = GeometryPoint.Create(x: 12.34, y: 56.78);
    
    // Create a 3D GeometryPoint (z is height).
    GeometryPoint point2 = GeometryPoint.Create(x: 12.34, y: 56.78, z: 9.0);
    
    // Create a 3D GeometryPoint (m is measures).
    GeometryPoint point3 = GeometryPoint.Create(x: 12.34, y: 56.78, z: 9.0, m: 321.0);
    
    // Create a 2D GeographyPoint.
    GeographyPoint point4 = GeographyPoint.Create(latitude: 12.34, longitude: 56.78);
    
    // Create a 3D GeographyPoint (z is elevation).
    GeographyPoint point5 = GeographyPoint.Create(latitude: 12.34, longitude: 56.78, z: 9.0);
    
    // Create a 3D GeographyPoint (m is measures).
    GeographyPoint point6 = GeographyPoint.Create(latitude: 12.34, longitude: 56.78, z: 9.0, m: 321.0);

    Spatial instances can be directly put into ODataPrimitiveValue as property values. Using the Address type from the last section:

    An ODataComplexValue for the Address type could be constructed as follows:

    var addressValue = new ODataComplexValue
    {
        Properties = new ODataProperty[]
        {
            new ODataProperty { Name = "Street", Value = new ODataPrimitiveValue("Zi Xing Rd") },
            new ODataProperty { Name = "City", Value = new ODataPrimitiveValue("Shanghai") },
            new ODataProperty { Name = "Postcode", Value = new ODataPrimitiveValue("200000") },
            new ODataProperty { Name = "GeometryLoc", Value = new ODataPrimitiveValue(GeometryPoint.Create(12.34, 56.78)) },
            new ODataProperty { Name = "GeographyLoc", Value = new ODataPrimitiveValue(GeographyPoint.Create(12.34, 56.78)) },
        }
    };

    Construct more complex spatial instances

    Directly creating these instances using Spatial APIs would be a bit complicated. So we highly recommend that you download and add the SpatialFactory.cs file to your project and use the GeometryFactory or the GeographyFactory class to construct more complex spatial instances.

    Here are some sample code of how to use the factory classes to create spatial instances:

    // Create a GeographyMultiPoint.
    GeographyMultiPoint multiPoint = GeographyFactory.MultiPoint().Point(-90.0, 0.0).Point(0.0, 90.0).Build();
    
    // Create a GeometryMultiPolygon.
    GeometryMultiPolygon multiPolygon = GeometryFactory.MultiPolygon()
        .Polygon().Ring(-5, -5).LineTo(0, -5).LineTo(0, -2)
        .Polygon().Ring(-10, -10).LineTo(-5, -10).LineTo(-5, -7).Build();
    
    // Create a GeometryLineString.
    GeometryLineString lineString = GeometryFactory.LineString(10, 20).LineTo(20, 30).Build();
    
    // Create a GeometryCollection.
    GeometryCollection collection = GeometryFactory.Collection()
        .MultiPoint().Point(5, 5).Point(10, 10)
        .LineString(0, 0).LineTo(0, 5)
        .Collection()
            .Point(5, 5);

    More samples could be found in the test cases of the Microsoft.Spatial.Tests project. Please find the source code here.

    References

    [Tutorial & Sample] Using Geospatial Data.

4. CLIENT

  • Basic CRUD Operations

    Request an entity set

    var context = new DefaultContainer(new Uri("http://services.odata.org/v4/(S(lqbvtwide0ngdev54adgc0lu))/TripPinServiceRW/"));
    
    var people = context.People.Execute();

    The Execute() API call will return an IEnumerable<Person>.

    Request an individual entity

    Either use the Where() API call:

    var people = context.People.Where(c => c.UserName == "russellwhyte");
    foreach (var person in people)
    {
        // business logic
    }

    or use the ByKey() API:

    var person = context.People.ByKey(userName: "russellwhyte").GetValue();

    or

    var person = context.People.ByKey(new Dictionary<string, object>() UserName).GetValue();

    The person object returned are all of the type Person.

    Update an entity

    var context = new DefaultContainer(new Uri("http://services.odata.org/v4/(S(lqbvtwide0ngdev54adgc0lu))/TripPinServiceRW/"));
    
    var person = context.People.ByKey(userName: "russellwhyte").GetValue(); // get an entity
    person.FirstName = "Ross"; // change its property
    context.UpdateObject(person); // create an update request
    
    context.SaveChanges(); // send the request

    Please be noted that the request is not sent until you call the SaveChanges() API. The context will track all the changes you make to the entities attached to it (by getting person from the service you attached it to the context) and will send requests for the changes when SaveChanges is called.

    The sample above will send a PATCH request to the service in which the body is the whole Person 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

    var context = new DefaultContainer(new Uri("http://services.odata.org/v4/(S(lqbvtwide0ngdev54adgc0lu))/TripPinServiceRW/"));
    
    var person = context.People.ByKey(userName: "russellwhyte").GetValue(); // get an entity
    context.DeleteObject(person); // create a delete request
    
    context.SaveChanges(); // send the request
  • Basic Queries Options

    $filter

    For GET http://host/service/EntitySet?$filter=Prop eq value:

    var people = context.People.Where(c => c.FirstName == "Peter");

    For GET http://host/service/EntitySet?$filter=endswith(Prop, value):

    var people = context.People.Where(c => c.FirstName.EndsWith("Peter"));

    For GET http://host/service/EntitySet?$filter=PropCol/$count eq value:

    var people = context.People.Where(c => c.Trips.Count == 2);

    For GET http://host/service/EntitySet?$filter=PropCol/any(d:d/Prop gt value):

    var people = context.People.Where(c => c.Trips.Any(d => d.Budget > 6000));

    $count

    For GET http://host/service/EntitySet/$count:

    var count = context.People.Count();

    For GET http://host/service/EntitySet?$count=true:

    var people = context.People.IncludeTotalCount();

    $orderby

    For GET http://host/service/EntitySet?$orderby=Prop:

    var people = context.People.OrderBy(c => c.FirstName);

    For GET http://host/service/EntitySet?$orderby=Prop desc:

    var people = context.People.OrderByDescending(c => c.FirstName);

    For GET http://host/service/EntitySet?$orderby=PropCol/$count:

    var people = context.People.OrderBy(c => c.Trips.Count);

    $skip

    var people = context.People.Skip(3);

    $top

    var people = context.People.Take(3);

    $expand

    var people = context.People.Expand(c => c.Trips);

    $select

    var people = context.People.Select(c => new {c.FirstName, c.LastName});

    A simple combined query combined

    var people =
        context.People.IncludeTotalCount()
            .Expand(c => c.Trips)
            .Where(c => c.FirstName == "Peter")
            .OrderBy(c => c.FirstName)
            .Skip(3)
            .Take(3);

    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 and DataServiceQueryContinuation<T>. They are classes that contain the next link of the partial set of items.

    Example:

    var context = new DefaultContainer(new Uri("http://services.odata.org/v4/TripPinServiceRW/"));
    
    // DataServiceQueryContinuation<T> contains the next link
    DataServiceQueryContinuation<Person> token = null;
    
    // Get the first page
    var response = context.People.Execute() as QueryOperationResponse<Person>;
    
    // Loop if there is a next link
    while ((token = response.GetContinuation()) != null)
    {
        // Get the next page
        response = context.Execute<Person>(token);
    }
  • 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:

    static void Main(string[] args)
    {
        var context = new DefaultContainer(new Uri("http://services.odata.org/v4/(S(b0vguruqwzqbmfoanwq1guxc))/TripPinServiceRW/"));
    
        var person = Person.CreatePerson("russell", "Russell", "Whyte", new long());
    
        context.AddToPeople(person);
    
        var responses = context.SaveChanges();
    
        foreach (var response in responses)
        {
            var changeResponse = (ChangeOperationResponse) response;
            var entityDescriptor = (EntityDescriptor) changeResponse.Descriptor;
            var personCreated = (Person) entityDescriptor.Entity; // the person created on the service
        }
    }
  • 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 call public 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 and qualifier. If the instance annotation doesn’t exist, it will try to get the metadata annotation of the fullQualifiedTermName and qualifier.

    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 inner QueryOperationResponse. ReceivingResponse is fired as many times as the responses are enumerated. So about the following code, before the client executes foreach, the code will only print the Content-Type for the top-level request. The last several lines of following code enumerates each of the QueryOperationResponse. 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 print

    200
    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 a Configurations property of DataServiceClientConfigurations which contains a RequestPipeline and ResponsePipeline. These two pipelines provide several hooks to developers to hook into the client request or response.

    OnMessageCreating

    OnMessageCreating is a property of the RequestPipeline.

    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 inherits HttpWebRequestMessage. HttpWebRequestMessage is a sub class of DataServiceClientRequestMessage

    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 the RequestPipeline.

    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 call AddProperties in OnEntryStarting.

    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, like ExecuteAsync;

    #Asynchronous Query #

    Query an Entity Set

    DataServiceQuery<TElement> provides BeginExecute and EndExecute methods to support query a collection of entities

    DefaultContainer 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 an IEnumerable<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 provides BeginExecute method which could take a DataServiceQueryContinuation<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 provides BeginGetValue and EndGetValue methods to support querying a single entity

    public 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 of DataServiceQuery<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 and EndLoadProperty 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 provides BeginExecuteBatch to put several query in a batch. The queries are specified as DataServiceRequest<TElement> instances. The EndExecuteBatch returns a DataServiceResponse that represents the response of the batch request as a whole. Individual query responses are represented as DataServiceResponse objects that can be accessed by enumerating the DataServiceResponse 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 provides BeginSaveChanges and EndSavechanges methods to asynchronously submits the pending changes to the data service. Changes are added to the DataServiceContext 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);

    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 provides SaveChangesAsync 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 provides BeginGetReadStream and EndGetReadStream 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 the ExecuteBatch method is called. It contains the query request objects.

    This method accepts an array of DataServiceRequest as parameters. It returns a DataServiceResponse object, which is a collection of QueryOperationResponse<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 the QueryOperationResponse<T> object for the operation that failed and the remaining operations are still executed.

        DefaultContainer dsc = new DefaultContainer(new Uri("http://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/"));
        public void BatchQuery()
        {
            var peopleQuery = dsc.People;
            var airlinesQuery = dsc.Airlines;
    
            var batchResponse = dsc.ExecuteBatch(peopleQuery, airlinesQuery);
            foreach(var r in batchResponse)
            {
                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);
                    }
                }
            }
        }

    ExecuteBatch will send a “POST” request to http://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 provides SaveChangesOptions.BatchWithSingleChangeset and SaveChangesOptions.BatchWithIndependentOperations when SaveChanges.

    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.

        DefaultContainer dsc = new DefaultContainer(new Uri("http://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/"));
        public void BatchModify()
        {
            dsc.MergeOption = MergeOption.PreserveChanges;
            var me = dsc.Me.GetValue();
            var myTrip = dsc.Me.Trips.First();
    
            me.LastName = "Test";
            myTrip.Description = "Updated Trip";
    
            dsc.UpdateObject(me);
            dsc.UpdateObject(myTrip);
    
            dsc.SaveChanges(SaveChangesOptions.BatchWithSingleChangeset);
    
            Console.WriteLine(me.LastName);
            Console.WriteLine(myTrip.Description);
        }

    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","AddressInfo@odata.type":"#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,"Emails@odata.type":"#Collection(String)","Emails":["April@example.com","April@contoso.com"],"FirstName":"April","Gender@odata.type":"#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","Tags@odata.type":"#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 of DataServiceCollectionOfT.

    Entity Tracking

    DataServiceContext provides several ways to track an entity.

    1. Newly added entities will be automatically tracked.
    2. If you use DataServiceContext.AttachTo to attach an entity, DataServiceContext will track the entity.
    3. Entities returned by queries are also tracked if DataServiceContext.MergeOption is not MergeOption.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 from DataServiceContext 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 methods AddRelatedObject, AttachLink, AddLink, SetLink, DetachLink, DeleteLink to track a link.

    One sample to use AttachTo and DeleteLink.

    	DefaultContainer dsc = new DefaultContainer(new Uri("http://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/"));
    
        public void ClientEntityTracking()
        {
            var person = new Person()
    	    {
    	        UserName = "clydeguess"
    	    };
    	
    	    var oneFriend = new Person()
    	    {
    	        UserName = "keithpinckney"
    	    };
    	    dsc.AttachTo("People", person);
    	    dsc.AttachTo("People", oneFriend);
    	    dsc.DeleteLink(person, "Friends", oneFriend);
    	    dsc.SaveChanges();
        }

    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 an IODataRequestMessage.

    Following is the code how to use OnMessageCreating.

    	public void UseHttpClientTest()
        {
            DefaultContainer dsc = new DefaultContainer(new Uri("http://services.odata.org/V4/(S(uvf1y321yx031rnxmcbqmlxw))/TripPinServiceRW/"));
    
            dsc.Configurations.RequestPipeline.OnMessageCreating = (args) =>
                {
                    var message = new HttpClientRequestMessage(args.ActualMethod) { Url = args.RequestUri, Method = args.Method, };
                    foreach (var header in args.Headers)
                    {
                        message.SetHeader(header.Key, header.Value);
                    }
    
                    return message;
                };
            var people = dsc.People.ToList();
            foreach (var p in people)
            {
                Console.WriteLine(p.FirstName);
            }
        }

    In this sample, we create a HttpClientRequestMessage instance in OnMessageCreating method. HttpClientRequestMessage is a class derived from DataServiceClientRequestMessage. In this class, we use MemoryStream to write data, and use HttpClient to get response. Once we get the HttpResponseMessage, we will convert it to IODataResponseMessage. So we also write a HttpClientResponseMessage class which implements IODataResponseMessage.

    	
    	public class HttpClientRequestMessage : DataServiceClientRequestMessage
    	{
    		private readonly HttpRequestMessage requestMessage;
            private readonly HttpClient client;
            private readonly HttpClientHandler handler;
            private readonly MemoryStream messageStream;
            private readonly Dictionary<string, string> contentHeaderValueCache;
    
            public HttpClientRequestMessage(string actualMethod)
                : base(actualMethod)
            {
                this.requestMessage = new HttpRequestMessage();
                this.messageStream = new MemoryStream();
                this.handler = new HttpClientHandler();
                this.client = new HttpClient(this.handler, disposeHandler: true);
                this.contentHeaderValueCache = new Dictionary<string, string>();
            }
    
            public override IEnumerable<KeyValuePair<string, string>> Headers
            {
                get
                {
                    if (this.requestMessage.Content != null)
                    {
                        return HttpHeadersToStringDictionary(this.requestMessage.Headers).Concat(HttpHeadersToStringDictionary(this.requestMessage.Content.Headers));
                    }
    
                    return HttpHeadersToStringDictionary(this.requestMessage.Headers).Concat(this.contentHeaderValueCache);
                }
            }
    
            public override Uri Url
            {
                get { return requestMessage.RequestUri; }
                set { requestMessage.RequestUri = value; }
            }
    
            public override string Method
            {
                get { return this.requestMessage.Method.ToString(); }
                set { this.requestMessage.Method = new HttpMethod(value); }
            }
    
            public override ICredentials Credentials
            {
                get { return this.handler.Credentials; }
                set { this.handler.Credentials = value; }
            }
    
            public override int Timeout
            {
                get { return (int)this.client.Timeout.TotalSeconds; }
                set { this.client.Timeout = new TimeSpan(0, 0, value); }
            }
    
            /// <summary>
            /// Gets or sets a value that indicates whether to send data in segments to the Internet resource. 
            /// </summary>
            public override bool SendChunked
            {
                get
                {
                    bool? transferEncodingChunked = this.requestMessage.Headers.TransferEncodingChunked;
                    return transferEncodingChunked.HasValue && transferEncodingChunked.Value;
                }
                set { this.requestMessage.Headers.TransferEncodingChunked = value; }
            }
    
            public override string GetHeader(string headerName)
            {
                //Returns the value of the header with the given name.
            }
    
            public override void SetHeader(string headerName, string headerValue)
            {
                // Sets the value of the header with the given name
            }
    
            public override Stream GetStream()
            {
                return this.messageStream;
            }
    
            /// <summary>
            /// Abort the current request.
            /// </summary>
            public override void Abort()
            {
                this.client.CancelPendingRequests();
            }
    
            public override IAsyncResult BeginGetRequestStream(AsyncCallback callback, object state)
            {
                var taskCompletionSource = new TaskCompletionSource<Stream>();
                taskCompletionSource.TrySetResult(this.messageStream);
                return taskCompletionSource.Task.ToApm(callback, state);
            }
    
            public override Stream EndGetRequestStream(IAsyncResult asyncResult)
            {
                return ((Task<Stream>)asyncResult).Result;
            }
    
            public override IAsyncResult BeginGetResponse(AsyncCallback callback, object state)
            {
                var send = CreateSendTask();
                return send.ToApm(callback, state);
            }
    
            public override IODataResponseMessage EndGetResponse(IAsyncResult asyncResult)
            {
                var result = ((Task<HttpResponseMessage>)asyncResult).Result;
                return ConvertHttpClientResponse(result);
            }
    
            public override IODataResponseMessage GetResponse()
            {
                var send = CreateSendTask();
                send.Wait();
                return ConvertHttpClientResponse(send.Result);
            }
    
            private Task<HttpResponseMessage> CreateSendTask()
            {
                // Only set the message content if the stream has been written to, otherwise
                // HttpClient will complain if it's a GET request.
                var messageContent = this.messageStream.ToArray();
                if (messageContent.Length > 0)
                {
                    this.requestMessage.Content = new ByteArrayContent(messageContent);
    
                    // Apply cached "Content" header values
                    foreach (var contentHeader in this.contentHeaderValueCache)
                    {
                        this.requestMessage.Content.Headers.Add(contentHeader.Key, contentHeader.Value);
                    }
                }
    
                this.requestMessage.Method = new HttpMethod(this.ActualMethod);
    
                var send = this.client.SendAsync(this.requestMessage);
                return send;
            }
    
            private static IDictionary<string, string> HttpHeadersToStringDictionary(HttpHeaders headers)
            {
                return headers.ToDictionary((h1) => h1.Key, (h2) => string.Join(",", h2.Value));
            }
    
            private static HttpClientResponseMessage ConvertHttpClientResponse(HttpResponseMessage response)
            {
                return new HttpClientResponseMessage(response);
            }
        }
    
        public static class TaskExtensionMethods
        {
            public static Task<TResult> ToApm<TResult>(this Task<TResult> task, AsyncCallback callback, object state)
            {
                var tcs = new TaskCompletionSource<TResult>(state);
    
                task.ContinueWith(
                    delegate
                    {
                        if (task.IsFaulted)
                        {
                            tcs.TrySetException(task.Exception.InnerExceptions);
                        }
                        else if (task.IsCanceled)
                        {
                            tcs.TrySetCanceled();
                        }
                        else
                        {
                            tcs.TrySetResult(task.Result);
                        }
    
                        if (callback != null)
                        {
                            callback(tcs.Task);
                        }
                    },
                    CancellationToken.None,
                    TaskContinuationOptions.None,
                    TaskScheduler.Default);
    
                return tcs.Task;
            }
        }
        public class HttpClientResponseMessage : IODataResponseMessage, IDisposable
        {
            private readonly IDictionary<string, string> headers;
            private readonly Func<Stream> getResponseStream;
            private readonly int statusCode;
    
        #if DEBUG
            /// <summary>set to true once the GetStream was called.</summary>
            private bool streamReturned;
        #endif
    
            public HttpClientResponseMessage(HttpResponseMessage httpResponse)
            {
                this.headers = HttpHeadersToStringDictionary(httpResponse.Headers);
                this.statusCode = (int)httpResponse.StatusCode;
                this.getResponseStream = () => { var task = httpResponse.Content.ReadAsStreamAsync(); task.Wait();  return task.Result; };
            }
    
            private static IDictionary<string, string> HttpHeadersToStringDictionary(HttpHeaders headers)
            {
                return headers.ToDictionary((h1) => h1.Key, (h2) => string.Join(",", h2.Value));
            }
    
            /// <summary>
            /// Returns the collection of response headers.
            /// </summary>
            public virtual IEnumerable<KeyValuePair<string, string>> Headers
            {
                get { return this.headers; }
            }
    
            /// <summary>
            /// The response status code.
            /// </summary>
            public virtual int StatusCode
            {
                get { return this.statusCode; }
    
                set { throw new NotSupportedException(); }
            }
    
            public virtual string GetHeader(string headerName)
            {
                string result;
                if (this.headers.TryGetValue(headerName, out result))
                {
                    return result;
                }
    
                // Since the unintialized value of ContentLength header is -1, we need to return
                // -1 if the content length header is not present
                if (string.Equals(headerName, "Content-Length", StringComparison.Ordinal))
                {
                    return "-1";
                }
    
                return null;
            }
    
            public virtual void SetHeader(string headerName, string headerValue)
            {
                if(String.IsNullOrEmpty(headerValue))
                {
                    return;
                }
                if (this.headers.ContainsKey(headerName))
                {
                    this.headers[headerName] = headerValue;
                }
                else
                {
                    this.headers.Add(headerName, headerValue);
                }
            }
    
            public virtual Stream GetStream()
            {
        #if DEBUG
                Debug.Assert(!this.streamReturned, "The GetStream can only be called once.");
                this.streamReturned = true;
        #endif
    
                return this.getResponseStream();
            }
        }

5. RELEASE NOTES

6. DESIGN

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:

    {dynamicProperty}

    Uri templates

    There are three kind of Uri template:

    1. Key template: ~/Customers({key})
    2. Function parameter template: ~/Customers/Default.MyFunction(name={name})
    3. Path template: ~/Customers/{dynamicProperty}

    Be caution:

    1. please EnableUriTemplateParsing = true for UriParser.
    2. Path template can’t be the first segment.

    Example

    var uriParser = new ODataUriParser(HardCodedTestModel.TestModel, new Uri("People({1})/{some}", UriKind.Relative))  
    {  
      EnableUriTemplateParsing = true  
    };
    
    var paths = uriParser.ParsePath().ToList();
    
    var keySegment = paths[1].As<KeySegment>();
    var templateSegment = paths[2].As<PathTemplateSegment>();
    templateSegment.LiteralText.Should().Be("{some}"); 
  • Add vocabulary annotations to EdmEnumMember

    From ODataLib 6.11.0, it supports to add vocabulary annotations to EdmEnumMember.

    Create Model

    EdmModel model = new EdmModel();
    
    EdmEntityContainer container = new EdmEntityContainer("DefaultNamespace", "Container");
    model.AddElement(container);
    
    EdmEntityType carType = new EdmEntityType("DefaultNamespace", "Car");
    EdmStructuralProperty carOwnerId = carType.AddStructuralProperty("OwnerId", EdmCoreModel.Instance.GetInt32(false));
    carType.AddKeys(carOwnerId);
    
    var colorType = new EdmEnumType("DefaultNamespace", "Color", true);
    model.AddElement(colorType);
    colorType.AddMember("Cyan", new EdmIntegerConstant(1));
    colorType.AddMember("Blue", new EdmIntegerConstant(2));
    
    EdmEnumMember enumMember = new EdmEnumMember(colorType, "Red", new EdmIntegerConstant(3));
    colorType.AddMember(enumMember);
    
    EdmTerm stringTerm = new EdmTerm("DefaultNamespace", "StringTerm", EdmCoreModel.Instance.GetString(true));
    model.AddElement(stringTerm);
    
    var annotation = new EdmAnnotation(
        enumMember,
        stringTerm,
        "q1",
        new EdmStringConstant("Hello world!"));
        model.AddVocabularyAnnotation(annotation);
    
    carType.AddStructuralProperty("Color", new EdmEnumTypeReference(colorType, false));
    
    container.AddEntitySet("Cars", carType);

    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

    var abstractType = new EdmEntityType("DefaultNS", "AbstractEntity", null, true, false);
    model.AddElement(abstractType);
    
    var orderType = new EdmEntityType("DefaultNS", "Order", abstractType);
    var orderIdProperty = new EdmStructuralProperty(orderType, "OrderID", EdmCoreModel.Instance.GetInt32(false));
    orderType.AddProperty(orderIdProperty);
    orderType.AddKeys(orderIdProperty);
    model.AddElement(orderType);

    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:

    [global::Microsoft.OData.Client.EntityType()]
    public abstract partial class AbstractEntity : global::Microsoft.OData.Client.BaseEntityType, global::System.ComponentModel.INotifyPropertyChanged
    {
     ...
    }
    
    [global::Microsoft.OData.Client.Key("OrderID")]
    [global::Microsoft.OData.Client.EntitySet("Orders")]
    public partial class Order : AbstractEntity
    {
     ...
    }
  • 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

    var requestMessage = new HttpWebRequestMessage(new Uri("http://example.com", UriKind.Absolute));
    requestMessage.PreferHeader().ContinueOnError = true;
    requestMessage.PreferHeader().MaxPageSize = 1024;
    requestMessage.PreferHeader().TrackChanges = true;

    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:

    ~/Customers?$skiptoken=abc

    We can do as follows to parse:

    var uriParser = new ODataUriParser(...);
    string token = parser.ParseSkipToken();
    Assert.Equal("abc", token);

    $deltatoken

    Let’s have an example:

    ~/Customers?$deltaToken=def

    We can do as follows to parse:

    var uriParser = new ODataUriParser(...);
    string token = parser.ParseDeltaToken();
    Assert.Equal("def", token);
  • 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:

    EdmEntityType customer = new EdmEntityType("NS", "Customer"); 
    customer.AddKeys(customer.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); 
    customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); 
    var ssn = customer.AddStructuralProperty("SSN", EdmPrimitiveTypeKind.String); 
    model.AddAlternateKeyAnnotation(customer, new Dictionary<string, IEdmProperty> 
    { 
        {"SSN", ssn} 
    }); 
    model.AddElement(customer);

    The following is the related metadata:

    <EntityType Name="Customer">
      <Key>  
        <PropertyRef Name="ID" />  
      </Key>
      <Property Name="ID" Type="Edm.Int32" Nullable="false" />
      <Property Name="Name" Type="Edm.String" />
      <Property Name="SSN" Type="Edm.String" />
      <Annotation Term="OData.Community.Keys.V1.AlternateKeys">
        <Collection>
         <Record Type="OData.Community.Keys.V1.AlternateKey">
          <PropertyValue Property="Key">
            <Collection>
              <Record Type="OData.Community.Keys.V1.PropertyRef">
                <PropertyValue Property="Alias" String="SocialSN" /> 
                <PropertyValue Property="Name" PropertyPath="SSN" />
              </Record>
            </Collection>
          </PropertyValue>
        </Record>
      </Annotation>
    </EntityType>

    Multiple alternate keys

    Edm Model builder

    The following codes can be used to build the multiple alternate keys:

    EdmEntityType order = new EdmEntityType("NS", "Order");
    order.AddKeys(order.AddStructuralProperty("OrderId", EdmPrimitiveTypeKind.Int32));
    var orderName = order.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String);
    var orderToken = order.AddStructuralProperty("Token", EdmPrimitiveTypeKind.Guid);
    order.AddStructuralProperty("Amount", EdmPrimitiveTypeKind.Int32);
    model.AddAlternateKeyAnnotation(order, new Dictionary<string, IEdmProperty>
    {
      {"Name", orderName}
    });
    
    model.AddAlternateKeyAnnotation(order, new Dictionary<string, IEdmProperty>
    {
      {"Token", orderToken}
    });
    
    model.AddElement(order);

    The following is the related metadata:

    <EntityType Name="Order">
    <Key>
      <PropertyRef Name="OrderId" />
    </Key>
    <Property Name="OrderId" Type="Edm.Int32" />
    <Property Name="Name" Type="Edm.String" />
    <Property Name="Token" Type="Edm.Guid" />
    <Property Name="Amount" Type="Edm.Int32" />
    <Annotation Term="OData.Community.Keys.V1.AlternateKeys">
      <Collection>
    	<Record Type="OData.Community.Keys.V1.AlternateKey">
    	  <PropertyValue Property="Key">
    		<Collection>
    		  <Record Type="OData.Community.Keys.V1.PropertyRef">
    			<PropertyValue Property="Alias" String="Name" />
    			<PropertyValue Property="Name" PropertyPath="Name" />
    		  </Record>
    		</Collection>
    	  </PropertyValue>
    	</Record>
    	<Record Type="OData.Community.Keys.V1.AlternateKey">
    	  <PropertyValue Property="Key">
    		<Collection>
    		  <Record Type="OData.Community.Keys.V1.PropertyRef">
    			<PropertyValue Property="Alias" String="Token" />
    			<PropertyValue Property="Name" PropertyPath="Token" />
    		  </Record>
    		</Collection>
    	  </PropertyValue>
    	</Record>
      </Collection>
    </Annotation>
    </EntityType>

    Composed alternate keys

    Edm Model builder

    The following codes can be used to build the multiple alternate keys:

    EdmEntityType person = new EdmEntityType("NS", "Person");
    person.AddKeys(person.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32));
    var country = person.AddStructuralProperty("Country", EdmPrimitiveTypeKind.String);
    var passport = person.AddStructuralProperty("Passport", EdmPrimitiveTypeKind.String);
    model.AddAlternateKeyAnnotation(person, new Dictionary<string, IEdmProperty>
    {
    	{"Country", country},
    	{"Passport", passport}
    });
    model.AddElement(person);

    The following is the related metadata:

    <EntityType Name="Person">
    <Key>
      <PropertyRef Name="ID" />
    </Key>
    <Property Name="ID" Type="Edm.Int32" />
    <Property Name="Country" Type="Edm.String" />
    <Property Name="Passport" Type="Edm.String" />
    <Annotation Term="OData.Community.Keys.V1.AlternateKeys">
      <Collection>
    	<Record Type="OData.Community.Keys.V1.AlternateKey">
    	  <PropertyValue Property="Key">
    		<Collection>
    		  <Record Type="OData.Community.Keys.V1.PropertyRef">
    			<PropertyValue Property="Alias" String="Country" />
    			<PropertyValue Property="Name" PropertyPath="Country" />
    		  </Record>
    		  <Record Type="OData.Community.Keys.V1.PropertyRef">
    			<PropertyValue Property="Alias" String="Passport" />
    			<PropertyValue Property="Name" PropertyPath="Passport" />
    		  </Record>
    		</Collection>
    	  </PropertyValue>
    	</Record>
      </Collection>
    </Annotation>
    </EntityType>

    Uri parser

    Enable the alternate keys parser extension via the Uri resolver AlternateKeysODataUriResolver.

    var parser = new ODataUriParser(model, new Uri("http://host"), new Uri("http://host/People(SocialSN = \'1\')"))
    {
        Resolver = new AlternateKeysODataUriResolver(model)
    };
  • 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:

    IEdmModel model = new EdmModel();

    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:

    SetVocabularyAnnotation

    Let’s have an example to illustrate how to use capabilities vocabulary:

    IEnumerable<IEdmProperty> requiredProperties = ...
    IEnumerable<IEdmProperty> nonFilterableProperties = ...
    requiredProperties = requiredProperties ?? EmptyStructuralProperties;  
    nonFilterableProperties = nonFilterableProperties ?? EmptyStructuralProperties;  
    
    IList<IEdmPropertyConstructor> properties = new List<IEdmPropertyConstructor>  
    {  
      new EdmPropertyConstructor(CapabilitiesVocabularyConstants.FilterRestrictionsFilterable, new EdmBooleanConstant(isFilterable)),
      new EdmPropertyConstructor(CapabilitiesVocabularyConstants.FilterRestrictionsRequiresFilter, new EdmBooleanConstant(isRequiresFilter)),
      new EdmPropertyConstructor(CapabilitiesVocabularyConstants.FilterRestrictionsRequiredProperties, new EdmCollectionExpression(
    	requiredProperties.Select(p => new EdmPropertyPathExpression(p.Name)).ToArray())),
      new EdmPropertyConstructor(CapabilitiesVocabularyConstants.FilterRestrictionsNonFilterableProperties, new EdmCollectionExpression(
        nonFilterableProperties.Select(p => new EdmPropertyPathExpression(p.Name)).ToArray()))
    }; 
    
    IEdmValueTerm term = model.FindValueTerm("Org.OData.Capabilities.V1.FilterRestrictions");  
    if (term != null)  
    {  
      IEdmRecordExpression record = new EdmRecordExpression(properties);  
      EdmAnnotation annotation = new EdmAnnotation(target, term, record);  
      annotation.SetSerializationLocation(model, EdmVocabularyAnnotationSerializationLocation.Inline);  
      model.SetVocabularyAnnotation(annotation);  
    }  

    The related metata

    The corresponding metadata can be as follows:

    <EntitySet Name="Customers" EntityType="NS"> 
    <Annotation Term="Org.OData.Capabilities.V1.FilterRestrictions">
       <Record>  
    	 <PropertyValue Property="Filterable" Bool="true" />
    	 <PropertyValue Property="RequiresFilter" Bool="true" />
    	 <PropertyValue Property="RequiredProperties">
    	   <Collection />
    	 </PropertyValue> 
    	 <PropertyValue Property="NonFilterableProperties">
    	   <Collection>  
    		<PropertyPath>Name</PropertyPath>
    		<PropertyPath>Orders</PropertyPath>
    		<PropertyPath>NotFilterableNotSortableLastName</PropertyPath>
    	   <PropertyPath>NonFilterableUnsortableLastName</PropertyPath>
    	  </Collection>
       </PropertyValue>
      </Record>
    </Annotation>
  • 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.

    ODataMessageWriter messageWriter = new ODataMessageWriter(...);
    IEdmTypeReference elementType = ...;
    ODataCollectionWriter writer = messageWriter.CreateODataCollectionWriter(elementType);
    
    ODataCollectionStart collectionStart = new ODataCollectionStart();
    
    collectionStart.NextPageLink = new Uri("http://any");
    collectionStart.Count = 5;
    
    writer.WriteStart(collectionStart);
    
    ODataCollectionValue collectionValue = ...;
    if (collectionValue != null)
    {
        foreach (object item in collectionValue.Items)
        {
            writer.WriteItem(item);
        }
    }
    writer.WriteEnd();

    The payload looks like:

    {
      "@odata.context":".../$metadata#Collection(...)",
      "@odata.count":5,
      "@odata.nextLink":"http://any",
      "value":[
        ...
      ]
    }
  • 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:

    <Function Name="GetAllProducts" IsComposable="true">
      <ReturnType Type="Collection(NS.Product)" Nullable="false" />
    </Function>
    <Action Name="Discount" IsBound="true" EntitySetPath="products">
      <Parameter Name="products" Type="Collection(NS.Product)" Nullable="false" />
      <Parameter Name="percentage" Type="Edm.Int32" Nullable="false" />
      <ReturnType Type="Collection(NS.Product)" Nullable="false" />
    </Action>
    ...
    <FunctionImport Name="GetAllProducts" Function="NS.GetAllProducts" EntitySet="Products" IncludeInServiceDocument="true" />

    GetAllProducts is a function import and it is composable. And since action Discount accepts what GetAllProducts returns, we can query Discount after GetAllProducts.

    1. Create function query

    var products = Context.CreateFunctionQuery<Product>("", "GetAllProducts", true).Execute();

    And we can append query option to the function. For example:

    var products = Context.CreateFunctionQuery<ProductPlus>("", "GetAllProducts", true).AddQueryOption("$select", "Name").Execute();

    The actual query would be:

    GET http://localhost/GetAllProducts()?$select=Name

    2. With codegen

    With OData client generator, proxy class for function and action would be auto generated. For example:

    var getAllProductsFunction = Context.GetAllProductsPlus();
    var products = getAllProductsFunction.Execute();   // Get products 
    var discdProducts = getAllProductsFunction.DiscountPlus(50).Execute();   // Call action on function
    var filteredProducts = getAllProductsFunction.Where(p => p.SkinColorPlus == ColorPlus.RedPlus).Execute();   //Add query option 

    Bound composable function

    Bound composable function has similiar usage, except that it is tied to a resource.

    For example, we have model:

    <Function Name="GetSeniorEmployees" IsBound="true" EntitySetPath="People" IsComposable="true">
        <Parameter Name="employees" Type="Collection(NS.Employee)" Nullable="false" />
        <ReturnType Type="NS.Employee" />
    </Function>
    <Function Name="GetHomeAddress" IsBound="true" IsComposable="true">
        <Parameter Name="person" Type="NS.Person" Nullable="false" />
        <ReturnType Type="NS.HomeAddress" Nullable="false" />
    </Function>

    Person is the base type of Employee. Then a sample query is:

    (Context.People.OfType<Employee>() as DataServiceQuery<Employee>).GetSeniorEmployees().GetHomeAddress().GetValue();
  • 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 implemementing ConvertFromPayloadValue and ConvertToPayloadValue.

    public class Microsoft.OData.Core.ODataPayloadValueConverter {
    	public ODataPayloadValueConverter ()
    
    	public virtual object ConvertFromPayloadValue (object value, Microsoft.OData.Edm.IEdmTypeReference edmTypeReference)
    	public virtual object ConvertToPayloadValue (object value, Microsoft.OData.Edm.IEdmTypeReference edmTypeReference)
    }

    New helper functions to set converter to model so the new converter can be applied:

      public static void SetPayloadValueConverter (Microsoft.OData.Edm.IEdmModel model, Microsoft.OData.Core.ODataPayloadValueConverter converter)
      public static Microsoft.OData.Core.ODataPayloadValueConverter GetPayloadValueConverter (Microsoft.OData.Edm.IEdmModel model)
    }

    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

    internal class DateTimeOffsetCustomFormatPrimitivePayloadValueConverter : ODataPayloadValueConverter
    {
        public override object ConvertToPayloadValue(object value, IEdmTypeReference edmTypeReference)
        {
            if (value is DateTimeOffset)
            {
                return ((DateTimeOffset)value).ToString("R", CultureInfo.InvariantCulture);
            }
    
            return base.ConvertToPayloadValue(value, edmTypeReference);
        }
    
        public override object ConvertFromPayloadValue(object value, IEdmTypeReference edmTypeReference)
        {
            if (edmTypeReference.IsDateTimeOffset() && value is string)
            {
                return DateTimeOffset.Parse((string)value, CultureInfo.InvariantCulture);
            }
    
            return base.ConvertFromPayloadValue(value, edmTypeReference);
        }
    }

    2. Set converter to model

    model.SetPayloadValueConverter(new DateTimeOffsetCustomFormatPrimitivePayloadValueConverter());

    Then DateTimeOffset can be serialized to Thu, 12 Apr 2012 18:43:10 GMT, and payload like Thu, 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 property Prop1 in the Entry. The implementation of InMemoryMessage in this sample can be found here.

    //Construct the model
    EdmModel model = new EdmModel();
    var entityType = new EdmEntityType("Namespace", "EntityType", null, false, false, false);
    entityType.AddKeys(entityType.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32));
    entityType.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(isNullable: true), null, EdmConcurrencyMode.Fixed);
    
    model.AddElement(entityType);
    
    var container = new EdmEntityContainer("Namespace", "Container");
    var entitySet = container.AddEntitySet("EntitySet", entityType);
    
    // Create ODataMessageWriterSettings in which set EnableFullValidation to false
    MemoryStream outputStream = new MemoryStream();
    IODataResponseMessage message = new InMemoryMessage() { Stream = outputStream };   
    message.SetHeader("Content-Type", "application/json;odata.metadata=minimal");
    ODataUri odataUri = new ODataUri()
    {
        ServiceRoot = new Uri("http://example.org/odata.svc"),
    };
    ODataMessageWriterSettings settings = new ODataMessageWriterSettings()
    {
        AutoComputePayloadMetadataInJson = true,
        EnableFullValidation = false,
        ODataUri = odataUri,
    };
    
    // Write the payload with extra property "Prop1"
    var entry = new ODataEntry
    {
        Properties = new[]
        {
            new ODataProperty { Name = "ID", Value = 102 },
            new ODataProperty { Name = "Name", Value = "Bob" },
            new ODataProperty { Name = "Prop1", Value = "Var1" }
        },
    };
    
    ODataItem[] itemsToWrite = { entry };            
    
    using (var messageWriter = new ODataMessageWriter(message, settings, model))
    {
        ODataWriter writer = messageWriter.CreateODataEntryWriter(entitySet, entityType);
        writer.WriteStart(entry);
        writer.WriteEnd();
    
        outputStream.Seek(0, SeekOrigin.Begin);
        var output = new StreamReader(outputStream).ReadToEnd();
        Console.WriteLine(output);
        Console.ReadLine();
    }

    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 in DataServiceContext.

    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.

    Context.SendingRequest2 += (sender, eventArgs) => ((HttpWebRequestMessage)eventArgs.RequestMessage).SetHeader("Prefer", "odata.include-annotations=*");
    
    // By default, DisableInstanceAnnotationMaterialization = false
    var people = Context.People.ByKey(1).GetValue();
    Context.TryGetAnnotation<string>(people.HomeAddress, "Microsoft.Test.OData.Services.ODataWCFService.AddressType", out annotation);
    Assert.AreEqual("Home", annotation);
    
    // Here we set DisableInstanceAnnotationMaterialization to true
    Context.DisableInstanceAnnotationMaterialization = true;
    
    people = Context.People.ByKey(1).GetValue();
    Context.TryGetAnnotation<string>(people.HomeAddress, "Microsoft.Test.OData.Services.ODataWCFService.AddressType", out annotation);
    Assert.AreEqual(null, annotation);        // We are not able to get any annotation out. 
  • 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:

    1. 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).

    2. 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.

    ODataMessageWriterSettings messageWriterSettings = new ODataMessageWriterSettings
    	    {
    	        Version = ODataVersion.V3,
    	        BaseUri = new Uri("http://example.com/"),
    	        UndeclaredPropertyBehaviorKinds = ODataUndeclaredPropertyBehaviorKinds.SupportUndeclaredValueProperty
    	    };
    const string payload = @"{""odata.metadata"":""http://www.example.com/#Server.NS.container1.serverEntitySet/@Element"",""Id"":123,"
              ""undeclaredComplex1"":{""property1"":""aa"",""property2"":""bb""}}";
    ODataEntry entry = ...; // read with messageWriterSettings
    ODataUntypedValue undeclaredComplex1Value= entry.Properties.Single(s => string.Equals(s.Name, "undeclaredComplex1"))
            .Value as ODataUntypedValue; // here undeclaredComplex1Value.RawJson is string "{\"property1\":\"aa\",\"property2\":\"bb\"}"
    	

    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:

    var context = new DefaultContainer(new Uri("http://services.odata.org/v4/(S(lqbvtwide0ngdev54adgc0lu))/TripPinServiceRW/"));
    context.Format.UseJson();
    context.UndeclaredPropertyBehavior = UndeclaredPropertyBehavior.Support;
    context.Configurations.ResponsePipeline.OnEntryEnded += (ReadingEntryArgs e) =>
        {
            // undeclared property can be accessed here (with the above UndeclaredPropertyBehavior.Support) :
            ODataProperty property = e.Entry.Properties.Single(s => string.Equals(s.Name, "..."));
    
        };
    var peopleQuery = from p in cxt.People select p;
    var peopleList = peopleQuery.ToList();
  • Use ODataSimplified Convention In ODataUriParser

    From ODataLib 6.14.0, we introduce ODataSimplified convention to make key-as-segment and default 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:

    var parser = new ODataUriParser(model, new Uri("http://www.potato.com/"), new Uri("http://www.potato.com/Schools/1/Student/Microsoft.Test.Taupo.OData.WCFService.Customer")) { UrlConventions = ODataUrlConventions.ODataSimplified };
    var result = parser.ParsePath();

    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.

    public abstract class Microsoft.OData.Core.ODataDeltaWriter
    {
        ...
        public abstract void WriteStart (Microsoft.OData.Core.ODataFeed expandedFeed)
        public abstract void WriteStart (Microsoft.OData.Core.ODataNavigationLink navigationLink) 
        public abstract System.Threading.Tasks.Task WriteStartAsync (Microsoft.OData.Core.ODataFeed expandedFeed)
        public abstract System.Threading.Tasks.Task WriteStartAsync (Microsoft.OData.Core.ODataNavigationLink navigationLink)
        ...
    }

    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 calling WriteStart(expandedFeed) to write an expanded feed. So is for a single expanded entity.

    ODataDeltaWriter writer = messageWriter.CreateODataDeltaWriter(customersEntitySet, customerType);
    writer.WriteStart(deltaFeed);               // delta feed
    writer.WriteStart(customerEntry);           // delta entity
    writer.WriteStart(ordersNavigationLink);    // navigation link
    writer.WriteStart(ordersFeed);              // normal expanded feed
    writer.WriteStart(orderEntry);              // normal entity
    writer.WriteEnd();  // orderEntry
    writer.WriteEnd(); // ordersFeed
    writer.WriteEnd(); // ordersNavigationLink
    writer.WriteEnd(); // customerEntry
    writer.WriteEnd(); // deltaFeed
    writer.Flush();
    
    string payloadLooksLike =
        "{" +
            "\"@odata.context\":\"http://host/service/$metadata#Customers/$delta\"," +
            "\"value\":" +
            "[" + // deltaFeed
                "{" + // customerEntry
                    "\"@odata.id\":\"http://host/service/Customers('BOTTM')\"," +
                    "\"ContactName\":\"Susan Halvenstern\"," +
                    "\"Orders\":" + // ordersNavigationLink
                    "[" + // ordersFeed
                        "{" + // orderEntry
                            "\"@odata.id\":\"http://host/service/Orders(10643)\"," +
                            "\"Id\":10643," +
                            "\"ShippingAddress\":" +
                            "{" +
                                "\"Street\":\"23 Tsawassen Blvd.\"," +
                                "\"City\":\"Tsawassen\"," +
                                "\"Region\":\"BC\"," +
                                "\"PostalCode\":\"T2F 8M4\"" +
                            "}" +
                        "}" +
                    "]" +
                "}" +
            "]"+
        "}";

    The next sample shows how to write a single expanded entity in a delta response.

    ODataDeltaWriter writer = messageWriter.CreateODataDeltaWriter(customersEntitySet, customerType);
    writer.WriteStart(deltaFeed);                           // delta feed
    writer.WriteStart(customerEntry);                       // delta entry
    writer.WriteStart(productBeingViewedNavigationLink);    // navigation link
    writer.WriteStart(productEntry);                        // normal expanded entry
    writer.WriteStart(detailsNavigationLink);               // nested navigation link
    writer.WriteStart(detailsFeed);                         // nested expanded feed
    writer.WriteStart(productDetailEntry);                  // normal entry
    writer.WriteEnd(); // productDetailEntry
    writer.WriteEnd(); // detailsFeed
    writer.WriteEnd(); // detailsNavigationLink
    writer.WriteEnd(); // productEntry
    writer.WriteEnd(); // productBeingViewedNavigationLink
    writer.WriteEnd(); // customerEntry
    writer.WriteEnd(); // deltaFeed
    writer.Flush();
    
    string payloadLooksLike =
        "{" +
            "\"@odata.context\":\"http://host/service/$metadata#Customers/$delta\"," +
            "\"value\":" +
            "[" +
                "{" +
                    "\"@odata.id\":\"http://host/service/Customers('BOTTM')\"," +
                    "\"ContactName\":\"Susan Halvenstern\"," +
                    "\"ProductBeingViewed\":" +
                    "{" +
                        "\"@odata.id\":\"http://host/service/Product(1)\"," +
                        "\"Id\":1," +
                        "\"Name\":\"Car\"," +
                        "\"Details\":" +
                        "[" +
                            "{" +
                                "\"@odata.type\":\"#MyNS.ProductDetail\"," +
                                "\"Id\":1," +
                                "\"Detail\":\"made in china\"" +
                            "}" +
                        "]" +
                    "}" +
                "}" +
            "]" +
        "}";

    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 to WriteStart(entry), WriteStart(feed) and WriteStart(navigationLink) are delegated to an internal ODataJsonLightWriter which is responsible for writing normal payloads. And the control will return to ODataJsonLightDeltaWriter 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.

    public enum Microsoft.OData.Core.ODataDeltaReaderState
    {
        ...
        ExpandedNavigationProperty = 10
        ...
    }
    
    public abstract class Microsoft.OData.Core.ODataDeltaReader
    {
        ...
        Microsoft.OData.Core.ODataReaderState SubState  { public abstract get; }
        ...
    }

    Note that the sub state is ODataReaderState which is used for normal payloads. The sub state is a complement to the main state in ODataDeltaReader to specify the detailed reader state within expanded navigation properties. But the sub state is ONLY available and meaningful when the main state is ODataDeltaReaderState.ExpandedNavigationProperty. Users can still use the Item 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.

    ODataDeltaReader reader = messageReader.CreateODataDeltaReader(customersEntitySet, customerType);
    while (reader.Read())
    {
        switch (reader.State)
        {
            case ODataDeltaReaderState.DeltaFeedStart:
                // Start delta feed
                ...
            case ODataDeltaReaderState.FeedEnd:
                // End delta feed
                ...
            case ODataDeltaReaderState.DeltaEntryStart:
                // Start $entity (may be followed by an ExpandedNavigationProperty)
                ...
            case ODataDeltaReaderState.DeltaEntryEnd:
                // End $entity
                ...
            case ODataDeltaReaderState.ExpandedNavigationProperty:
                switch (reader.SubState)
                {
                    case ODataReaderState.FeedStart:
                        var feed = reader.Item as ODataFeed;
                        ...
                    case ODataReaderState.EntryStart:
                        var entry = reader.Item as ODataEntry;
                        ...
                    case ODataReaderState.NavigationLinkStart:
                        var navigationLink = reader.Item as ODataNavigationLink;
                        ...
                    case ODataReaderState.Start:
                        // Start the expanded payload
                        ...
                    case ODataReaderState.Completed:
                        // Finish the expanded payload
                        ...
                    ...
                }
                break;
            ...
        }
    }

    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 inside ODataJsonLightReader to skip parsing the context URLs thus each ODataEntry 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 the ExpandedNavigationProperty state until it detects the state of the internal reader to be Completed. However we still leave the Start and Completed 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

    #463

    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 properties Filter and OrderBy 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:

    public class TypeFacetsPromotionRules
    {
        public virtual int? GetPromotedPrecision(int? left, int? right);
        public virtual int? GetPromotedScale(int? left, int? right);
    }

    The first method defines the rule to resolve two precision values left and right 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:

    1. if both left and right are null, return null;
    2. if only one is null, return the other;
    3. otherwise, return the larger.

    You plugin a specific set of conversion rules by setting the ODataUriResolver.TypeFacetsPromotionRules property that has a declared type of TypeFacetsPromotionRules. If not explicitly specified by the user, an instance of the base class TypeFacetsPromotionRules will be used by default.

    Let’s see a simple example. Consider the expression Decimal_6_3 mul Decimal_5_4 where Decimal_6_3 and Decimal_5_4 are both structural properties of Edm.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:

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

    internal IList<ODataPathSegment> ParsePath(ICollection<string> segments)
    {
        // ...
        try
        {
            while (this.TryGetNextSegmentText(out segmentText))
            {
                if (this.parsedSegments.Count == 0)
                {
                    this.CreateFirstSegment(segmentText);
                }
                else
                {
                    this.CreateNextSegment(segmentText);
                }
            }
        }
        catch (ODataUnrecognizedPathException ex)
        {
            ex.ParsedSegments = this.parsedSegments;
            ex.CurrentSegment = segmentText;
            ex.UnparsedSegments = this.segmentQueue.ToList();
            throw ex;
        }
        // ...
    }
    
    private void CreateNextSegment(string text)
    {
        if (this.TryHandleAsKeySegment(text))
        {
            return;
        }
    
        if (this.TryCreateValueSegment(text))
        {
            return;
        }
            
        ...
    
        if (this.TryCreateTypeNameSegment(previous, identifier, parenthesisExpression))
        {
            return;
        }
    
        if (this.TryCreateSegmentForOperation(previous, identifier, parenthesisExpression))
        {
            return;
        }
    }

    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:

    If this segment is the special escape-marker segment, then remember that the next segment cannot be a key, 
    even if we are in key-as-segments mode. Essentially, it is an escape into 'metadata-space', so to speak. 
    DEVNOTE: We went back and forth several times on whether this should be allowed everywhere or only 
    where a key could appear. We landed on allowing it absolutely everywhere for several reasons: 
      1) The WCF DS client naively adds the escape marker before all type segments, regardless of whether the prior segment is a collection. 
      2) The WCF DS server already allowed the escape marker almost everywhere in 5.3 
      3) It's better to be either extremely loose or extremely strict than allow it in some cases and not in others.
    Note that this is not publicly documented in OData V3 nor is it planned to be documented in OData V4, but it is a part of supporting the Key-As-Segment conventions that are used by many Azure services.

    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:

    public class ODataUriParserContext
    {
        public readonly ODataUriParserSettings Settings;
        public readonly IEdmModel Model;
        public readonly Uri ServiceRoot;
        public readonly ODataUrlConventions UrlConventions;
        public readonly Func<string, BatchReferenceSegment> BatchReferenceCallback;
        public readonly bool EnableUriTemplateParsing;
    
        public List<ODataPathSegment> ParsedSegments { get; set; }
        public Queue<string> RawSegments { get; set; }
    }

    2.2 Update Parser configuration

    Modify the following class:

    public sealed class ODataUriParser
    {
        public delegate void ODataPathSegmentHandler(ODataUriParserContext context);
        public ODataPathSegmentHandler OnSegmentParsing { get; set; }
        // ...
    }

    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

    public sealed class ODataUrlConventions{
        public static ODataUrlConventions Default { get; }
        public static ODataUrlConventions KeyAsSegment { get; }
        public static ODataUrlConventions ODataSimplified { get; }
    }

    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

    public enum ODataUndeclaredPropertyBehaviorKinds
    {
    	None,
    	IgnoreUndeclaredValueProperty,
    	ReportUndeclaredLinkProperty,
    	SupportUndeclaredValueProperty
    }

    2.2 New class to represent untyped JSON element

    public class ODataUntypedValue : ODataValue
    {
    	public string ODataType { get; set;}
    	public string JsonString { get; set;}
    }

    2.3 Reader support

    Property kind: known odata.type

    • Example
    {
    	"data":{
    		"@odata.type":"ns.known",
    		"p1":1
    	}
    }
    • 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:
    {
    	"data":1
    }
    • Behaviour
    Default Ignore Untyped JSON
    Exception(Non-open), Read (Open) Ignore(Non-Open), Read (Open) Read

    Property kind: Unknown odata.type

    • Example
    {
    	"data":{
    		"@odata.type":"ns.unknown",
    		"p1":1
    	}
    }
    • Behaviour
    Default Ignore Untyped JSON
    Exception Ignore Read as untyped

    Property kind: no odata.type with non-primitive value

    • Example
    {
    	"data":{
    		"d1":"p2"
    	}
    }
    • 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
    {
    "data":{
    "@odata.type":"ns.known"
    "p1":1
    }
    }

    -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
    {
    “data”:1
    }			

    -Behaviour

    Default Ignore Untyped JSON
    Exception(Non-open), Write (Open) Ignore(Non-Open), Write (Open) Write

    Property kind: Unknown odata.type

    • Example
    {
    “data”:{
    “@odata.type”:”ns.unknown”
    “p1:1
    }
    }			

    -Behaviour

    Default Ignore Untyped JSON
    Exception Ignore Write as untyped JSON

    Property kind: no odata.type with non primitive value

    • Example
    {
    “data”:{
    “d1:”p2
    }
    }			

    -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.

    public sealed class ODataMessageWriterSettings : ODataMessageWriterSettingsBase
    {
    	public ODataUndeclaredPropertyBehaviorKinds UndeclaredPropertyBehaviorKinds {get; set;}
    }

    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.

    public EdmNavigationProperty AddUnidirectionalNavigation(EdmNavigationPropertyInfo propertyInfo)
    public EdmNavigationProperty AddBidirectionalNavigation(EdmNavigationPropertyInfo propertyInfo, EdmNavigationPropertyInfo partnerInfo)
    private EdmNavigationPropertyInfo FixUpDefaultPartnerInfo(EdmNavigationPropertyInfo propertyInfo, EdmNavigationPropertyInfo partnerInfo)

    Then, developers can call as follows to define the navigation property on complex type, for example:

    EdmEntityType customer = new EdmEntityType(NS, Customer);
    .
    EdmComplexType address = new EdmComplexType("NS", "Address");
    
    address.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo
    {
         Name = "Customer",
         TargetMultiplicity = EdmMultiplicity.ZeroOrOne,
         Target = customer
    });

    2 Modify EdmNavigationProperty class.

    • a) Change the private constructor to accept the IEdmStructuredType.
    • b) Change DeclaringEntityType property as following:
    public IEdmEntityType DeclaringEntityType
    {
      get
      {
        if (DeclaringType is IEdmEntityType)
        {
          return (IEdmEntityType)this.DeclaringType;
        }
        return null;
      }
    }
    • c) Add a similar property named DeclaringComplexType, the implementation is same as #b.
    • d) Add new public APIs as follows:
    public static EdmNavigationProperty CreateNavigationProperty(IEdmComplexType declaringType, EdmNavigationPropertyInfo propertyInfo)
    private static EdmNavigationProperty CreateNavigationProperty(IEdmStructuredType declaringType, EdmNavigationPropertyInfo propertyInfo)
    • e) Make the original CreateNavigationProperty() function and new added public API for complex type to call the new added private function, same as follows:
    public static EdmNavigationProperty CreateNavigationProperty(IEdmEntityType declaringType, EdmNavigationPropertyInfo propertyInfo)
    {
      return CreateNavigationProperty((IEdmStructuredType)declaringType, propertyInfo);
    }

    3 Add the following extension methods:

    public static IEnumerable<IEdmNavigationProperty> DeclaredNavigationProperties(this IEdmComplexType type)
    public static IEnumerable<IEdmNavigationProperty> NavigationProperties(this IEdmComplexType type)
    public static IEnumerable<IEdmNavigationProperty> NavigationProperties(this IEdmComplexTypeReference type)
    public static IEnumerable<IEdmNavigationProperty> DeclaredNavigationProperties(this IEdmComplexTypeReference type)
    public static IEdmNavigationProperty FindNavigationProperty(this IEdmComplexTypeReference type, string name)

    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:

    protected override void ProcessComplexType(IEdmComplexType element)
    {
      this.BeginElement(element, this.schemaWriter.WriteComplexTypeElementHeader);
    
      this.VisitProperties(element.DeclaredStructuralProperties());
      this.VisitProperties(element.DeclaredNavigationProperties());
    
      this.EndElement(element);
    }

    Then, the complex type in metadata document may have navigation property. Let’s have an example:

      <ComplexType Name="Address">
        <Property Name="Street" Type="Edm.String" /><Property Name="Country" Type="Edm.String" />
        <NavigationProperty Name="Customer" Type="NS.Customer" />
      </ComplexType>

    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:

    protected CsdlNamedStructuredType(string name, string baseTypeName, bool isAbstract, bool isOpen, IEnumerable<CsdlProperty> properties,   IEnumerable<CsdlNavigationProperty> navigationProperties, CsdlDocumentation documentation, CsdlLocation location)
      : base(properties, navigationProperties, documentation, location)
    {
      
    }
    • Modify CreateRootElementParser() function in CsdlDocumentParser. Add the following templates for Complex type element:
    //// <NavigationProperty>
    CsdlElement<CsdlNamedElement>(CsdlConstants.Element_NavigationProperty, this.OnNavigationPropertyElement, documentationParser,
      //// <ReferentialConstraint/>
      CsdlElement<CsdlReferentialConstraint>(CsdlConstants.Element_ReferentialConstraint, this.OnReferentialConstraintElement, documentationParser),
      //// <OnDelete/>
      CsdlElement<CsdlOnDelete>(CsdlConstants.Element_OnDelete, this.OnDeleteActionElement, documentationParser),
        //// <Annotation/>
        annotationParser),
    • 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:

    13.4.1 Attribute Path
    A navigation property binding MUST name a navigation property of the entity sets, singleton's, or containment navigation property's entity type or one of its subtypes in the Path attribute. If the navigation property is defined on a subtype, the path attribute MUST contain the QualifiedName of the subtype, followed by a forward slash, followed by the navigation property name. If the navigation property is defined on a complex type used in the definition of the entity sets entity type, the path attribute MUST contain a forward-slash separated list of complex property names and qualified type names that describe the path leading to the navigation property.

    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.

    Customer (Entity)
    {
      
      Address Location;
    }
    
    Address (Complex)
    {
      
      City City;
    }
    
    City (Entity)
    {}
    
    EntitySet: Customers (Customer)
    EntitySet: Cities (City)

    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:

    public void AddNavigationTarget(IEdmNavigationProperty property, IEdmNavigationSource target, IList<IEdmStructuralProperty> path)
    or
    public void AddNavigationTarget(IEdmNavigationProperty property, IEdmNavigationSource target, IList<string> path)

    Let’s have a detail example to illustrate how the users (developers) to add the navigation binding:

    1) Add a complex type:

    // complex type address
     EdmComplexType address = new EdmComplexType("NS", "Address");
     address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String);
          
     model.AddElement(address);

    2) Add “Customer” entity type:

    EdmEntityType customer = new EdmEntityType("NS", "Customer");
    customer.AddKeys(customer.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32));
    
    var location = customer.AddStructuralProperty("Location", new EdmComplexTypeReference(address, isNullable: true));
    model.AddElement(customer);

    3) Add “City” entity type

    EdmEntityType city = new EdmEntityType("NS", "City");
    city.AddKeys(city.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32));
    
    model.AddElement(city);	

    4) Add a navigation property for “Address” complex type

    EdmNavigationProperty addressNavProp = address.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo
    {
    Name = "City",
    TargetMultiplicity = EdmMultiplicity.ZeroOrOne,
    Target = city
    });

    5) Add an entity set and the navigation property binding

    EdmEntityContainer container = new EdmEntityContainer("NS", "Default");
    model.AddElement(container);
    EdmEntitySet customers = container.AddEntitySet("Customers", customer);
    EdmEntitySet cities = container.AddEntitySet(Cities, city);
    customers.AddNavigationTarget(addressNavProp, cities, new[] { location });

    6) Therefore, we can have the navigation property binding as:

      <EntityContainer Name="Default">
        <EntitySet Name="Customers" EntityType="NS.Customer">
          <NavigationPropertyBinding Path="Location/NS.Address/City" Target="Cities" />
        </EntitySet>
      </EntityContainer>

    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:

    private void CreatePropertySegment(ODataPathSegment previous, IEdmProperty property, string queryPortion)
    {
      
      segment.TargetEdmNavigationSource = previous.TargetEdmNavigationSource;
      
    }

    Let’s have a request example:

    http://localhost/Orders(1)/Location/City

    Then, the result of Uri parser can be:

    ~/EntitySetSegment/KeySegment/PropertySegment/NavigationPropertySegment

    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:

    1. Add a flag for Uri resolver to configure whether the navigation property is allowed in complex type
    2. Modify GenerateExpandItem(ExpandTermToken) function in SelectExpandBinder
    3. Modify SelectEpxandPathBinder to add a new function to process the property segment in expand clause.

    Let’s have an example: ** $expand=Location/City **

    IDictionary<string, string> queryOptions = new Dictionary<string, string>
      {
        { "$expand", "Location/City" }
      };
    var _order = _model.SchemaElements.OfType<IEdmEntityType>().FirstOrDefault(e => e.Name == "Order");
    var _orders = _model.FindDeclaredEntitySet("Orders");
    var parser = new ODataQueryOptionParser(_model, _order, _orders, queryOptions);
    
    var selectAndExpand = parser.ParseSelectAndExpand();

    Then, selectAndExpand has one SelectedItems with the following OData path with segments:

    1. PropertySegment
    2. 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:

    public void WriteStart(ODataExpandableProperty property);

    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:

    ODataEntry entry1 = new ODataEntry();
    entry1.AddProperty(property); // normal properties
    
    ODataExpandableProperty expandableProperty = new ODataExpandableProperty();// the property with the NP.
    ODataEntry entry2 = new ODataEntry(); // Expanded entry belongs to above expandable property
    ODataNavigationLink navigationLink =  
    writer.WriteStart(entry1); // write start of entry1 and normal properties
    writer.WriteStart(expandableProperty); // write the expandable property
        writer.WriteStart(navigationLink);
            	writer.WriteStart(entry2); 
    writer.WriteEnd();   // End entry2
             writer.WriteEnd();  // End navigationLink
        writer.WriteEnd();  // End pr expandableProperty
    writer.WriteEnd(); // End entry1

    So, we can do as follows:

    1. Create a new class
    public sealed class ODataExpandableProperty : ODataItem
    {
       
    }

    Basically, ODataExpandableProperty can have the same structure of ODataProperty, but it should be derived from ODataItem, or it can be embedded with ODataProperty.

    1. Add two new abstract APIs in ODataWirter
    public abstract void WriteStart(ODataExpandableProperty property);
    public abstract Task WriteStartAsync(ODataExpandableProperty property);

    The users (developers, service) can call these APIs to write the expandable property.

    1. In ODataWriterCore, give an implementation for the above new abstract APIs.
    2. Add new item in WriterState enum type:
    internal enum WriterState
    {
    
      /// <summary>The writer is currently writing an expandable property.</summary>
      ExpanableProperty,
    
    }
    1. Add two new abstract API as follows in ODataWirterCore
    protected abstract void StartExpandableProperty(ODataExpandableProperty property);
    protected abstract void EndExpandableProperty(ODataExpandableProperty property);

    Then, to implement them in ODataAtomWriter & ODataJsonLightWriter. So far, leave the implementation in ODataAtomWriter to throw NotImplementedException.

    1. In ODataJsonLightWriter, we can have the following prototype codes:
    protected override void StartExpandableProperty(ODataExpandableProperty property)
    {
            
    }
    1. In OdataJsonLightPropertySerializer, add new internal API WriteExpandableProperty(…) to write the structural properties of the expandable property.
    2. 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:

    ODataEntry order = new ODataEntry()
    {
        TypeName = "NS.Order",
        Properties = new[]
        {
            new ODataProperty {Name = "ID", Value = 1},
            new ODataProperty {Name = "Amount", Value = 20}
        },
    };
    
    ODataEntry city = new ODataEntry()
    {
        TypeName = "NS.City",
        Properties = new[]
        {
            new ODataProperty {Name = "Name", Value = "Minhang"},
            new ODataProperty {Name = "State", Value = "Shanghai"},
            new ODataProperty {Name = "Country", Value = "CN"}
        }
    };
    
    ODataComplexValue location = new ODataComplexValue
    {
        Properties = new[]
        {
            new ODataProperty { Name = "Street", Value = "ZiXing Rd" },
            new ODataProperty { Name = "ZipCode", Value = "9001"}
        },
        TypeName = "NS.Address"
    };
    
    ODataProperty complex = new ODataProperty
    {
        Name = "Location",
        Value = location
    };
    
    ODataExpandableProperty expandable = new ODataExpandableProperty
    {
        Property = complex
    };
    
    ODataNavigationLink navigationLink = new ODataNavigationLink
    {
        Name = "City",
        IsCollection = false
    };

    Then, we can write the navigation property in complex type as:

    writer.WriteStart(order);
              writer.WriteStart(expandable);
    
                    writer.WriteStart(navigationLink);
    
                        writer.WriteStart(city);
                        writer.WriteEnd();
    
                    writer.WriteEnd(); // end of navigation link
              writer.WriteEnd();// end of expandable property
     writer.WriteEnd();

    We can have the following payload:

    {
      "@odata.context":"http://.../$metadata#Orders/$entity",
      "ID":1,
      "Amount":20,
      "Location":{
           "Street":"ZiXing Rd",
           "ZipCode":"9001",
           "City":{
                 "Name":"Minhang",
                 "State":"Shanghai",
                 "Country":"CN"
        }
      }
    }

    2.2.2.3 Collection expandable property in entry

    We can reuse the ODataExpandableProperty class to serialize the collection expandable property. For example:

    ODataComplexValue address1 = new ODataComplexValue()
    ODataComplexValue address2 = new ODataComplexValue()
    ODataProperty addresses = new ODataProperty
    {
      Name = "Addresses",
      Value = new ODataCollectionValue
      {
        TypeName = "Collection(NS.Address)", 
       Items = new[] { address1, address2 }
      }
    };
    ODataExpandableProperty expandable = new ODataExpandableProperty
    {
      Property = addresses
    };

    Then the service side codes can be as follows:

    ODataEntry entry1 = new ODataEntry();
    entry1.AddProperty(property); // normal properties
    
    ODataExpandableProperty expandableProperty = new ODataExpandableProperty();// the property with the NP.
    ODataEntry entry2 = new ODataEntry(); // Expanded entry belongs to above expandable property
    ODataNavigationLink navigationLink =  
    writer.WriteStart(entry1); // write start of entry1 and normal properties
    writer.WriteStart(expandableProperty); // write the expandable collection property
        foreach (item in collection expandable property)
        {
                ODataExpandableProperty expandable = new ODataExpandableProperty
                writer.WrtierStart(expandable);
                     writer.WriteStart(navigationLink);
            	           writer.WriteStart(entry2); 
               writer.WriteEnd();   // End entry2
                         writer.WriteEnd();  // End navigationLink
                    writer.WriteEnd();  // End expandable item
             }
        writer.WriterEnd(); // End of collection expandable item
    writer.WriteEnd(); // End entry1

    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:

    public abstract class ODataExpandblePropertyWriter
    {
      public abstract void WriteStart(ODataExpandableProperty property);
      public abstract void WriteEnd();
      
    }

    And make the implementation in a new class as follows:

    internal sealed class ODataJsonLightExpandblePropertyWriter : ODataExpandblePropertyWriter, IODataOutputInStreamErrorListener
    {
      public override void WriteStart(ODataExpandableProperty property)
      {
        
      }
    
      public override void WriteEnd()
      {
        
      }
    }

    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.

    public abstract class ODataCollectionExpandablePropertyWriter
    {
    
      public abstract void WriteStart();
      public abstract void WriteItem();
      public abstract void EndItem();
      public abstract void WriteEnd();
      
    }

    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:

    public enum ODataReaderState
    {
          
         ExpandablePropertyStart,
    
         ExpandablePropertyEnd,
         
    }

    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.

    while (reader.Read())
    {
         switch (reader.State)
         {
             case ODataReaderState.EntryStart:
                  break;
    
             ……
    
            case ODataReaderState.ExpandablePropertyStart:
                  break;
    
            case ODataReaderState.ExpandablePropertyEnd:
                  break;
    
    
            ……
            
            default:
                 break;
         }
    }

    Based on this design, we should do as follows:

    1. Add the following APIs in ODataReaderCore to start and end reading the expandable property.
    protected abstract bool ReadAtExpandablePropertyStartImplementation();
    protected abstract bool ReadAtExpandablePropertyEndImplementation();
    1. Need a new Scope to identify the expandable property reading
    private sealed class JsonLightExpandablePropertyScope : Scope
    {
        
    }

    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:

    public abstract class ODataExpandblePropertyReader
    {
      public abstract bool Read();  
      
    }

    And the implementation:

    internal sealed class ODataJsonLightExpandblePropertyReader : ODataExpandblePropertyReader
    {
    
      public override void Read ()
      {
        
      }
    }

    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.

    public abstract class ODataCollectionExpandablePropertyReader
    {
    
      public abstract void Read();
      
    }

    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
      context.Locations.Expand(a=>a.Address.City); 
      context.Locations.Bykey(1).Address.Expand(a=>a.City);   
      context.Locations.Bykey(1).Addresses.Expand(a=>a.City);
    1. LoadProperty
      var location = context.Locations.Where(l=>l.ID == 1).Single();
      var address = location.Address;
      context.LoadProperty(address, "City");
    1. AddRelatedObject, UpdateRelatedObject
      var city = new City();
      context.AddRelatedObject(address, Cities, city);
    1. 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.
    public class ExpandableComplexPropertyMaterializationPolicy : StructuralValueMaterializationPolicy
    {
      private readonly EntryValueMaterializationPolicy entryValueMaterializationPolicy;
      
    }
    1. Add MaterializerExpandableProperty to remember the materializer state of a given ODataExpandableProperty.
    internal class MaterializerExpandableProperty
    {
      /// <summary>The property.</summary>
      private readonly ODataExpandableProperty ;
    
      /// <summary>List of navigation links for this entry.</summary>
      private ICollection<ODataNavigationLink> navigationLinks = ODataMaterializer.EmptyLinks;
    
      
    }

    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:
    do
                    {
                        bool inComplexPropertyScope = false;
                        ODataExpandableProperty complexProperty;
                        ICollection<ODataNavigationLink> complexPropertyNavigationLinks = ODataMaterializer.EmptyLinks;
    
                        switch (this.reader.State)
                        {
                            case ODataReaderState.NavigationLinkStart:
                                if (!inExpandablePropertyScope)
                                {
                                    navigationLinks.Add(this.ReadNavigationLink());
                                }
                                else
                                {
                                    complexPropertyNavigationLinks.Add(this.ReadNavigationLink());
                                }
                                break;
                            case ODataReaderState.EntryEnd:
                                break;
                            case ODataReaderState.ExpandablePropertyStart:
                                complexProperty = (ODataExpandableProperty)this.reader.Item;
                                entry.AddComplexProperties(complexProperty);
                                inComplexPropertyScope = true;
                                break;
     
                            case ODataReaderState.ExpandablePropertyEnd:
                                inComplexPropertyScope = false; 
                                MaterializerExpandableProperty.CreateInstance(complexProperty, complexPropertyNavigationLinks);
                                complexPropertyNavigationLinks = ODataMaterializer.EmptyLinks;
                                break;
                            . 
    
                        }
                }

    Then the data flow would be like:

    1. Materialize The materializer will call EntryValueMaterializationPolicy to materialize an entity, and in EntryValueMaterializationPolicy, we can call ExpandableComplexPropertyMaterializationPolicy to handle complex type having navigation property.
    foreach (var property in entry.complexProperties)
    {
    MaterializerExpandableProperty materializerProperty = property.GetAnnotation< MaterializerExpandableProperty>();             this.expandablePropertyMaterializationPolicy.MaterializeExpandableProperty(property, materializerProperty. navigationLinks);
    object value = property.GetMaterializedValue();
        var prop = actualType.GetProperty(property.Name, this.MaterializerContext.IgnoreMissingProperties);
        prop.SetValue(entry.ResolvedObject, value, property.Name, true /* allowAdd? */);   
    }
    1. 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:

    1. Get existing complex type instance from descriptor (Refer to the tracking part)
    2. Read the payload to ODataEntry or ODataFeed
    3. 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.

    company.TotalAssetsPlus = 100;                  
    TestClientContext.UpdateObject(company);       
    TestClientContext.SaveChanges();

    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:

    public sealed class EntityDescriptor : Descriptor
    {
        private Uri identity;          // The id of the entity
    
            private object entity;    // The materialized value of the entity
    
            private Uri addToUri;    // uri of the resource set to add the entity to during save
    
            private Uri selfLink;      // uri to query the entity
     
            private Uri editLink;     // uri to edit the entity.
    
            private Dictionary<string, LinkInfo> relatedEntityLinks;    // Contains the LinkInfo (navigation and relationship links) for navigation properties
    
    }

    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).

    1. Create ComplexTypeDescriptor In order to reuse current logic of EntityDescriptor, we can add a base class ResourceDescriptor let EntityDescriptor and ComplexTypeDescriptor inherit from it.

    1. Then for EntiyTracker, it will be like:

    1. 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.

    1. We cannot track collection-value complex type property, so following scenario does not work:
    var location = TestClientContext.Locations.Where(lo=>lo.ID == 1).Single();
    var addresses = location.Addresses;    //Addresses is collection-value complex type property
    foreach (var addr in addresses) 
    {
      TestClientContext.LoadProperty(addr, "City");  // 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);

    1. 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:
    var addr = TestClientContext.CreateQuery<Address>("Company/Address").Execute();
    foreach (var d in addr)          // Will materialize Address here. 
    {
      TestClientContext.LoadProperty(d, "City");   // Does not work
    }

    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.

    1. 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
    2. 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
    3. 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:
    var city = context.Cities.Bykey(1).GetValue();
    Address address = new Address {Street = "Zixing"};
    Location location = new Location {ID=11, Address=address};
    context.AddToLocations(location);
    context.SetLink(address, "City", city);
    context.SaveChanges();
    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.
    
    1. 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:

    1. Generate Class for Address:

    If City is single-value navigation property:

    public partial class Address : global::System.ComponentModel.INotifyPropertyChanged
    {
        public global::Microsoft.OData.Client.City City  { get; set; }
    }
    public partial class AddressSingle : global::Microsoft.OData.Client.DataServiceQuerySingle<Address>
    {
        public global::Microsoft.OData.Client.CitySingle City { get; }
    }

    If City is collection-value navigation property:

    public partial class Address : global::System.ComponentModel.INotifyPropertyChanged
    {
        public global::Microsoft.OData.Client.DataServiceCollection<global::ODataClientSample.City> Cities  { get; set; }
    }
    public partial class AddressSingle : global::Microsoft.OData.Client.DataServiceQuerySingle<Address>
    {
        public global::Microsoft.OData.Client.DataServiceQuery<global::ODataClientSample.City> Cities { get; }
    }
    1. Add Address to Location: If complex type Address is single-value:
    public partial class Location: global::Microsoft.OData.Client.BaseEntityType, global::System.ComponentModel.INotifyPropertyChanged
    {
    public global::ODataClientSample.Address Address {get;set;}
    }

    Then we can support:

    var location = context.Locations.Where(l=>l.ID == 1).Single();
    var address = location.Address;

    If complex type Address is collection-value:

    public partial class Location: global::Microsoft.OData.Client.BaseEntityType, global::System.ComponentModel.INotifyPropertyChanged
    {
        public global::System.Collections.ObjectModel.ObservableCollection<Address> Addresses {get;set;}
    }

    Then we can support:

    var addresses = location.Addresses;
    1. Add Address to LocationSingle: If complex type Address is single-value:
    public partial class LocationSingle: global::Microsoft.OData.Client.DataServiceQuerySingle<Location>
    {
      public global::ODataClientSample.AddressSingle Address {get;}
    }

    Then we can support:

    var location = context.Location.ByKey(1).Address.Expand(a=>a.City);

    If complex type Address is collection-value:

    public partial class LocationSingle: global::Microsoft.OData.Client.DataServiceQuerySingle<Location>
    {
    public global::Microsoft.OData.Client.DataServiceQuery<global::ODataClientSample.Address> Addresses {get;}
    }

    Then we can support:

      var location = context.Location.ByKey(1).Addresses.Expand(a=>a.City);

    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:

    protected internal IDictionary<PropertyInfo, PropertyConfiguration> ExplicitProperties { get; private set; }

    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:

    1. Promote the following public/private APIs from EntityTypeConfiguration to StructuredTypeConfiguration.
    public virtual NavigationPropertyConfiguration AddNavigationProperty(PropertyInfo navigationProperty, EdmMultiplicity multiplicity)
    public virtual NavigationPropertyConfiguration AddContainedNavigationProperty(PropertyInfo navigationProperty, EdmMultiplicity multiplicity)
    private NavigationPropertyConfiguration AddNavigationProperty(PropertyInfo navigationProperty, EdmMultiplicity multiplicity, bool containsTarget)
    1. Promote the following property from EntityTypeConfiguration to StructuredTypeConfiguration
    public virtual IEnumerable<NavigationPropertyConfiguration> NavigationProperties
    1. Modify NaviatonPropertyConfiguration class, for example
      • modify DeclaringEntityType property
      • add DeclaringComplexType property
      • modify the constructor
    2. Modify EdmTypeBuilder class to construct the complex type with navigation property.
             Change
    private void CreateNavigationProperty(EntityTypeConfiguration config)
            To
    private void CreateNavigationProperty(StructuralTypeConfiguration config)
    1. Promote the following APIs from EntityTypeConfigurationOfTEntityType to StructuralTypeConfigurationOfTStrucuturalType.
      * HasMany
      * HasRequired
      * HasOptional

    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

    public class Customer
    {
       public int CustomerId { get; set; }
       public Address Location { get; set; }
    }
    
    public class Address
    {
       public string Street { get; set; }
       public Region Region { get; set; }
    }
    
    public class Region
    {
       public int RegionId { get; set; }
       public string Name { get; set; }
    }

    Then, we can configure the Edm type by non-convention model builder as:

    var builder = new ODataModelBuilder();
    builder.EntityType<Customer>().HasKey(c => c.CustomerId).ComplexProperty(c => c.Location);
    builder.EntityType<Region>().HasKey(r => r.RegionId).Property(r => r.Name);
    var address = builder.ComplexType<Address>();
    address.Property(a => a.Street);
    address.HasRequired(a => a.Region);

    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:

    1. Modify MapComplexType(…) function, for any implicated added structural types from complex type, mark it as entity type and add them as navigation properties.

    2. 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:

    var builder = new ODataConventionModelBuilder();
    builder.EntityType<Customer>();
    builder.ComplexType<Address>();

    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:

    builder.EntitySet<Customer>("Customers").HasRequiredBinding(c => c.Location.Region, "Regions");

    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

    1. 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:

    GET ~/Customers(1)?$expand=Location/Region

    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:

    public ISet<Tuple<IList<IEdmStructuralProperty>, IEdmNavigationProperty, SelectExpandClause>> ExpandedStructuralProperties { get; private set; }

    And we should construct this property in constructor of SelectExpandNode class.

    1. 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.

    var expandableProperties = selectExpandNode.ExpandedStructuralProperties.Select(e => e.Item1.First());
    var selectedStructuralProperties = selectExpandNode.SelectedStructuralProperties.Except(expandableProperties);
    ODataEntry entry = new ODataEntry
    {
       TypeName = typeName,
       Properties = CreateStructuralPropertyBag(selectedStructuralProperties, entityInstanceContext),
     };

    Then, the expanded structural properties exclude from the properties.

    b) Provide a new private API to write the expanded structural properties:

    private void WriteExpandedStructuralProperties(
                ISet<Tuple<IList<IEdmStructuralProperty>, IEdmNavigationProperty, SelectExpandClause>>
                    structuralPropetiesToExpand,
                EntityInstanceContext entityInstanceContext,
                ODataWriter writer)
    {
          Foreach( expanded structureal property)
          {
                Construct an expandable property (new ODataExpandableProperty())
                WriteStart(expandable property)
                WriteExpandedNavigationProperties(..)
                WriteEnd()
         }
    }

    c) Call WriteExpandedStructuralProperties after WriteStart(entry)

    ODataEntry entry = CreateEntry(selectExpandNode, entityInstanceContext);
    if (entry != null)
    {
         writer.WriteStart(entry);
         WriteExpandedStructuralProperties(selectExpandNode.ExpandedStructuralProperties, entityInstanceContext, writer);
          WriteNavigationLinks(selectExpandNode.SelectedNavigationProperties, entityInstanceContext, writer);
          WriteExpandedNavigationProperties(selectExpandNode.ExpandedNavigationProperties, entityInstanceContext, writer);
          writer.WriteEnd();
     }

    2.4.5.2 Top level expanded complex property

    We can modify ODataComplexTypeSerializer to support expanded complex property. For example:

    GET ~/Customers(1)/Location?$expand=Region

    So, we can do as follows:

    1. Modify EntityInstanceContext to accept complex type instance, or create a new class named ComplexInstanceContext.

    2. Use the SelectExpandNode into ODataComplexTypeSerializer

    3. If ExpandedStructuralProperties is empty, serialize the complex as normal, otherwise we will serialize an expandable property.

    4. Add new function named CreateExpandableProperty

    private static ODataExpandableProperty CreateExpandableProperty(EntityInstanceContext context)
    {
            .            
    }
    1. Create a new function to write the expandable property
    private static void WriteExpandableProperty(ODataExpandableProperty property, EntityInstanceContext context, ODataSerializerContext writeContext);
    {
            Create ODataExpandablePropertyWriter;
            Call WriteStart();
            WriteExpandedNavigationProperties();
            WriteEnd();
                      
     }

    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:

    public enum ODataReaderState
    {
          
         ExpandablePropertyStart,
    
         ExpandablePropertyEnd,
         
    }

    So, we can use them to read the expandable property.

    1. Create a new class named ODataExpandablePropertyWithNavigationLinks
    public sealed class ODataExpandablePropertyWithNavigationLinks : ODataItemBase
    {
           ……
            public ODataExpandableProperty Property
            {
                get { return Item as ODataExpandableProperty; }
            }
    
            public IList<ODataNavigationLinkWithItems> NavigationLinks { get; private set; }
     }
    1. Modify ReadEntryOrFeed() function in ODataEntityDeserializer by added two case statements into:
    while (reader.Read())
    {
           switch (reader.State)
            {
                  .
                  case ODataReaderState.ExpandablePropertyStart:
                            ....
                           break;
    
                   case ODataReaderState.ExpandablePropertyEnd:
                           break;
      
             }
    }

    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.

    1. Should modify ODataEntryWithNavigationLinks to accept the ODataExpandablePropertyWithNavigationLinks

    2. Add new API named AddExpandedStrucutralProperties()

    private void AddExpandedStrucutralProperties(IEdmNavigationProperty navigationProperty, object entityResource,
                ODataEntryWithNavigationLinks entry, ODataDeserializerContext readContext)
    {
      
    }

    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.

    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.