1. CORE

  • 1.1 Write OData payload

    There are several kinds of OData payloads, including service document, model metadata, entity set, entity, entity reference(s), complex value(s), primitive value(s). OData Core library is designed to write and read all these payloads.

    We’ll go through each kind of payload here. But first, let’s set up the necessary code that is common to all kinds of payloads.

    Class ODataMessageWriter is the entrance class to write OData payloads.

    To construct an ODataMessageWriter instance, you’ll need to provide an IODataResponseMessage, or IODataRequestMessage, depending on if you are writing a response or a request.

    OData Core library provides no implementation of these two interfaces, because it is different in different scenarios.

    In this tutorial, we’ll use the InMemoryMessage.cs.

    We’ll use the model set up in the EDMLIB section.

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

    Then set up the message to write the payload to.

    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 have written the payload, we can inspect the memory stream wrapped in InMemoryMessage to check what has been written.

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

    Here is the complete program that uses 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 writing each kind of payload.

    Write metadata document

    Writing metadata is simple, just use ODataMessageWriter.WriteMetadataDocument().

    writer.WriteMetadataDocument();

    Please notice that this API only works when:

    1. Writing a response message, i.e., when constructing ODataMessageWriter, you must supply IODataResponseMessage.
    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, settings);
    writer.WriteMetadataDocument();

    Write service document

    To write a service document, first create an ODataServiceDocument instance, which encapsulates all the necessary information in a service document, which includes entity sets, singletons, and function imports.

    In this example, we create a service document that contains two entity sets, one singleton, and one function import.

    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() to write it.

    writer.WriteServiceDocument(serviceDocument);

    However, this would not work. An ODataException will be thrown saying that “The ServiceRoot property in ODataMessageWriterSettings.ODataUri must be set when writing a payload.” This is because a valid service document will contain a context URL referencing the metadata document URL which needs to be provided in ODataMessageWriterSettings.

    The service root information is provided in ODataUri.ServiceRoot:

    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 a model to write a service document.

    It takes efforts to instantiate a service document instance and set up the entity sets, singletons, and function imports. Actually, EdmLib provides a useful API which can generate an appropriately-filled service document instance from model. The API is GenerateServiceDocument() defined as an extension method 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 appear in the generated service document. And according to the spec, only those function imports without any parameters should set their IncludeInServiceDocument attribute to true.

    As with WriteMetadataDocument(), WriteServiceDocument() works only when writing response messages.

    Besides WriteServiceDocument(), there is also WriteServiceDocumentAsync() in ODataMessageWriter. It is the async version of WriteServiceDocument(), so you can call it in an async way:

    await writer.WriteServiceDocumentAsync(serviceDocument);

    A lot of APIs in writer and reader provide async versions. They work as async counterparts to the APIs without the Async suffix.

    Write entity set

    An entity set is a collection of entities. Unlike metadata or service document, you must create another writer from ODataMessageWriter to write an entity set. The library is designed to write entity set in a streaming/progressive way, which means the entities are written one by one.

    Entity set is represented by the ODataResourceSet class. To write an entity set, the following information needs to be provided:

    1. The service root which is defined by ODataUri.
    2. The model, as provided when constructing the ODataMessageWriter instance.
    3. Entity set and entity type information.

    Here is how to write an empty entity set using the old WriteStart()/WriteEnd() API (for the new writer API, see here).

    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.CreateODataResourceSetWriter(entitySet);
    ODataResourceSet set = new ODataResourceSet();
    odataWriter.WriteStart(set);
    odataWriter.WriteEnd();

    Line 4 gives the service root, line 6 gives the model, and line 10 gives the entity set and entity type information.

    The output looks like

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

    The output contains a context URL which is based on the service root provided in ODataUri and the entity set name. There is also a value which turns out to be an empty collection. It will hold the entities if there is any.

    There is another way to provide the entity set and entity type information, through ODataResourceSerializationInfo. This also eliminates the need to provide a model.

    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.CreateODataResourceSetWriter();
    ODataResourceSet set = new ODataResourceSet();            
    set.SetSerializationInfo(new ODataResourceSerializationInfo
    {
        NavigationSourceName = "Customers",
        NavigationSourceEntityTypeName = "Customer"
    });
    odataWriter.WriteStart(set);
    odataWriter.WriteEnd();

    When writing entity set, you can provide a next page used in server driven paging.

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

    The output will then contain 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 appear after the value collection, you can set the next link after the WriteStart() call, but before the WriteEnd() call.

    ODataResourceSet set = new ODataResourceSet();
    odataWriter.WriteStart(set);
    set.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 additional requirement on next link, as long as it is a valid URL.

    To write an entity in an entity set, create an ODataResource instance and call WriteStart()/WriteEnd() on it in-between the WriteStart()/WriteEnd() calls on the entity set.

    ODataResourceSet set = new ODataResourceSet();
    odataWriter.WriteStart(set);
    ODataResource entity = new ODataResource
    {
        Properties = new[]
        {
            new ODataProperty
            {
                Name = "Id",
                Value = 1
            },
            new ODataProperty
            {
                Name = "Name",
                Value = "Tom"
            }
        }
    };
    odataWriter.WriteStart(entity);
    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 writing entities in the next section.

    Write entity

    Entities can be written in several places:

    1. As a top level entity.
    2. As an entity in an entity set.
    3. As an entity expanded within another entity.

    To write a top level entity, use ODataMessageWriter.CreateODataResourceWriter().

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

    We’ve already introduced in the previous section how to write entities in an entity set. Now we’ll look at how to write an entity expanded within another entity.

    ODataMessageWriter writer = new ODataMessageWriter((IODataResponseMessage)message, settings, model);
    IEdmEntitySet entitySet = model.FindDeclaredEntitySet("Customers");
    ODataWriter odataWriter = writer.CreateODataResourceWriter(entitySet);
    ODataResource customerEntity = new ODataResource
    {
        Properties = new[]
        {
            new ODataProperty
            {
                Name = "Id",
                Value = 1
            },
            new ODataProperty
            {
                Name = "Name",
                Value = "Tom"
            }
        }
    };
    ODataResource orderEntity = new ODataResource
    {
        Properties = new[]
        {
            new ODataProperty
            {
                Name = "Id",
                Value = 1
            },
            new ODataProperty
            {
                Name = "Price",
                Value = 3.14M
            }
        }
    };
    odataWriter.WriteStart(customerEntity);
    odataWriter.WriteStart(new ODataNestedResourceInfo
    {
        Name = "Purchases",
        IsCollection = true
    });
    odataWriter.WriteStart(new ODataResourceSet());
    odataWriter.WriteStart(orderEntity);
    odataWriter.WriteEnd();
    odataWriter.WriteEnd();
    odataWriter.WriteEnd();
    odataWriter.WriteEnd();

    The output will contain an order entity nested within a 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 Fluent functional-style writer API

    In the previous section, paired WriteStart()/WriteEnd() calls have been made to write payloads. In this version, a new set of fluent functional-style API has been introduced as an improvement over the previous API which is rather primitive, requiring paired WriteStart()/WriteEnd() calls.

    The new API replaces paired WriteStart()/WriteEnd() calls with a single Write() call. Write() comes in two flavors. The first flavor takes a single argument which is the thing you want to write. For example, writer.Write(entry); is equivalent to

    writer.WriteStart(entry);
    writer.WriteEnd();

    The second flavor takes two arguments. The first argument is same as before. The second argument is an Action delegate which is to be invoked in-between writing the first argument. For instance,

    writer.Write(outer, () => writer
        .Write(inner1)
        .Write(inner2));

    is equivalent to

    writer.WriteStart(outer);
        writer.WriteStart(inner1);
        writer.WriteEnd();
        writer.WriteStart(inner2);
        writer.WriteEnd();
    writer.WriteEnd();

    In general, this new API should be preferred to the previous one.

  • 1.3 Read OData payload

    The reader API is similar to the writer API, and you can expect symmetry here.

    First, we’ll set up the necessary code that is common to all kinds of payloads.

    Class ODataMessageReader is the entrance class to read OData payloads.

    To construct an ODataMessageReader instance, you’ll need to provide an IODataResponseMessage or IODataRequestMessage, depending on if you are reading a response or a request.

    OData Core library provides no implementation of these two interfaces, because it is different in different scenarios.

    In this tutorial, we’ll still use the InMemoryMessage.cs.

    We’ll still use the model set up in the EDMLIB section.

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

    Then set up the message to read the payload from.

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

    Create the settings:

    ODataMessageReaderSettings settings = new ODataMessageReaderSettings();

    Now we are ready to create an ODataMessageReader instance:

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

    We’ll use the code in the previous section to write the payloads, and in this section use the reader to read them. After writing the payloads, we should set MemoryStream.Position to zero.

    stream.Position = 0;

    Here is the complete program that uses SampleModelBuilder and InMemoryMessage to write and then read a metadata document:

    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 each kind of payload.

    Read metadata document

    Reading metadata is simple, just use ODataMessageReader.ReadMetadataDocument().

    reader.ReadMetadataDocument();

    Similar to writing a metadata document, this API only works when reading a response message, i.e., when constructing the ODataMessageReader, you must supply IODataResponseMessage.

    Read service document

    Reading service document is through the ODataMessageReader.ReadServiceDocument() API.

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

    This works only when reading a response message.

    There is another API ODataMessageReader.ReadServiceDocumentAsync(). It is the async version of ReadServiceDocument(), and you can call it in an async way:

    ODataServiceDocument serviceDocument = await reader.ReadServiceDocumentAsync();

    Read entity set

    To read an entity set, you must create another reader ODataResourceSetReader. The library is designed to read entity set in a streaming/progressive way, which means the entities are read one by one.

    Here is how to read an entity set.

    ODataMessageReader reader = new ODataMessageReader((IODataResponseMessage)message, readerSettings, model);
    ODataReader setReader = reader.CreateODataResourceSetReader(entitySet, entitySet.EntityType());
    while (setReader.Read())
    {
        switch (setReader.State)
        {
            case ODataReaderState.ResourceSetEnd:
                ODataResourceSet setFromReader = (ODataResourceSet)setReader.Item;
                break;
            case ODataReaderState.ResourceEnd:
                ODataResource entityFromReader = (ODataResource)setReader.Item;
                break;
        }
    }

    Read entity

    To read a top level entity, use ODataMessageReader.CreateODataResourceReader(). Except that, there is no difference compared to reading an entity set.

    ODataMessageReader reader = new ODataMessageReader((IODataResponseMessage)message, readerSettings, model);
    ODataReader entityReader = reader.CreateODataResourceReader(entitySet, entitySet.EntityType());
    while (entityReader.Read())
    {
        switch (entityReader.State)
        {
            case ODataReaderState.ResourceSetEnd:
                ODataResourceSet setFromReader = (ODataResourceSet)entityReader.Item;
                break;
            case ODataReaderState.ResourceEnd:
                ODataResource entityFromReader = (ODataResource)entityReader.Item;
                break;
        }
    }
  • 1.4 Use ODataUriParser

    This post is intended to guide you through the URI parser for OData V4, which is released with ODataLib V6.0 and later.

    You may have already read the following posts about OData URI parser in ODataLib V5.x:

    Parts of those articles, e.g., introductions to ODataPath and QueryNode hierarchy, still apply to the V4 URI parser. In this post, we will deal with API changes and newly-added features.

    Overview

    The main reference document for using URI parser is the URL Conventions specification. The ODataUriParser class is the main part of its implementation in ODataLib.

    The responsibility of ODataUriParser is two-fold:

    • Parse resource path
    • Parse query options

    We’ve also introduced the new ODataQueryOptionParser class in ODataLib V6.2+ to deal with the scenario where you do not have the full resource path and only want to parse the query options. The ODataQueryOptionParser shares the same API signatures for parsing query options. You can find more information below.

    Using ODataUriParser

    The use of ODataUriParser is easy and straightforward. As already mentioned, we do not support static methods now, so we will begin by creating an ODataUriParser instance.

    One ODataUriParser constructor is:

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

    Parameters:

    model is the data model the parser will refer to; serviceRoot is the base URI for the service, which is constant for a particular service. Note that serviceRoot must be an absolute URI; uri is the request URI to be parsed, including any query options. When it is an absolute URI, it must be based on serviceRoot, or it can be a relative URI. In the following example, we use the model from OData V4 demo service, and create an ODataUriParser instance based on it.

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

    Parsing resource path

    You can use the following API to parse resource path:

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

    You don’t need to pass in resource path as a parameter to ParsePath(), because it has already been provided when constructing the ODataUriParser instance.

    ODataPath holds an enumeration of path segments for the resource path. All path segments are represented by classes derived from ODataPathSegment.

    In the example, the resource path in the request URI is Products(1), so the resulting ODataPath will contain two segments: an EntitySetSegment for the entity set Products, and a KeySegment for the 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 represented by an instance of class XXXClause which presents the query option as an Abstract Syntax Tree (with semantic information bound). Note that $select and $expand query options are handled together by the SelectExpandClause class. The latter three all have primitive type values, and the parsing results are represented by the corresponding nullable primitive types.

    For all query option parsing results, a null value indicates that the corresponding query option is not present in the request URI.

    Here is an example for parsing the request URI with different kinds of query options (please notice that the value of skip would be null, since the skip query option is not present in the request URI):

    Uri requestUri = 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, requestUri);
    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 structures for SelectExpandClause, FilterClause, OrdeyByClause have already been presented in two previous articles mentioned in the beginning of this post. Here I’d like to talk about the newly-added SearchClause.

    SearchClause contains a tree representation of the $search query. The detailed rules of $search query option can be found here. In general, the search query string can contain search terms combined with logic operators: AND, OR and NOT.

    All search terms are represented by SearchTermNode which is derived from SingleValueNode. SearchTermNode has a Text property containing the original word or phrase.

    The SearchClause.Expression property holds the tree structure for $search. If $search contains a single word, the Expression would be a single SearchTermNode. But when $search contains a combination of various terms and logic operators, Expression would also contain BinaryOperatorNode and UnaryOperatorNode.

    For example, if the query option 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 ODataQueryOptionParser

    There may be cases where you already know the query context information, and does not have the full request URI. The ODataUriParser will not be available at this time, as it requires a full URI. The user would have to fake one.

    In ODataLib 6.2 we shipped a new URI parser that targets query options only. It requires the model and type information to be provided through its constructor, and then it could be used for query options parsing just as ODataUriParser.

    One of its constructors looks like this:

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

    Parameters (here the target object refers to what resource path is addressed, see spec):

    model is the model the parser will refer to; targetEdmType is the type of the target object, to which the query options apply; targetNavigationSource is the entity set or singleton where the target comes from, and it is usually the navigation source of the target object; queryOptions is a dictionary containing the key-value pairs for query options.

    Here is an example demonstrating its use. It is almost identical to that of ODataUriParser:

    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
  • 1.5 Dependency Injection Support

    From ODataLib v7.0, we introduced Dependency Injection (or “DI” in short) support to dramatically increase the extensibility of the library where users can plug in their custom implementations and policies in an elegant way. Introduction of DI can also simplify the API and implementation of ODataLib by eliminating redundant function parameters and class properties. Since ODataLib is a reusable library, we don’t take direct dependency on any existing DI framework. Instead we build and rely on an abstraction layer including several simple interfaces that decouples ODataLib from any concrete DI implementation. Users of ODataLib will be free to choose whatever DI framework they like to work with ODataLib.

    Introduction to DI

    For a complete understanding of the concept of DI and how it works in a typical ASP.NET Web application, please refer to the introduction from ASP.NET Core.

    To make DI work properly with ODataLib, basically there are several things you have to do within your application:

    • Implement your container builder based on your DI framework.
    • Register the required services from both ODataLib and your application.
    • Build and use the container (to retrieve the services) in ODataLib.


    Implement Your Container Builder

    Since ODataLib is based on .NET, we use the interface IServiceProvider from .NET Framework as the abstraction of “container”. The container itself is read-only (as you can see, IServiceProvider only has a GetService method) so we designed another interface IContainerBuilder in ODataLib to build the container. Below is the source of IContainerBuilder:

    public interface IContainerBuilder
    {
        IContainerBuilder AddService(
            ServiceLifetime lifetime,
            Type serviceType,
            Type implementationType);
    
        IContainerBuilder AddService(
            ServiceLifetime lifetime,
            Type serviceType,
            Func<IServiceProvider, object> implementationFactory);
    
        IServiceProvider BuildContainer();
    }

    The first AddService method registers a service by its implementation type while the second one registers using a factory method. The BuildContainer method is used to build a container that implements IServiceProvider which contains all the services registered. The first parameter of AddService indicates the lifetime of the service you register. Below is the source of ServiceLifetime. For the meaning of each member, please refer to the doc from ASP.NET Core.

    public enum ServiceLifetime
    {
        Singleton,
        Scoped,
        Transient
    }

    Once you have determined a specific DI framework to use in your application, you need implement a container builder from IContainerBuilder based on the DI framework you choose. In this tutorial, we will use the Microsoft DI Framework (the default DI implementation for ASP.NET Core) as an example. The implementation of the container builder should more or less look like below:

    using System;
    using System.Diagnostics;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.OData;
    using ServiceLifetime = Microsoft.Extensions.DependencyInjection.ServiceLifetime;
    
    public class TestContainerBuilder : IContainerBuilder
    {
        private readonly IServiceCollection services = new ServiceCollection();
    
        public IContainerBuilder AddService(
            Microsoft.OData.ServiceLifetime lifetime,
            Type serviceType,
            Type implementationType)
        {
            Debug.Assert(serviceType != null, "serviceType != null");
            Debug.Assert(implementationType != null, "implementationType != null");
    
            services.Add(new ServiceDescriptor(
                serviceType, implementationType, TranslateServiceLifetime(lifetime)));
    
            return this;
        }
    
        public IContainerBuilder AddService(
            Microsoft.OData.ServiceLifetime lifetime,
            Type serviceType,
            Func<IServiceProvider, object> implementationFactory)
        {
            Debug.Assert(serviceType != null, "serviceType != null");
            Debug.Assert(implementationFactory != null, "implementationFactory != null");
    
            services.Add(new ServiceDescriptor(
                serviceType, implementationFactory, TranslateServiceLifetime(lifetime)));
    
            return this;
        }
    
        public IServiceProvider BuildContainer()
        {
            return services.BuildServiceProvider();
        }
    
        private static ServiceLifetime TranslateServiceLifetime(
            Microsoft.OData.ServiceLifetime lifetime)
        {
            switch (lifetime)
            {
                case Microsoft.OData.ServiceLifetime.Scoped:
                    return ServiceLifetime.Scoped;
                case Microsoft.OData.ServiceLifetime.Singleton:
                    return ServiceLifetime.Singleton;
                default:
                    return ServiceLifetime.Transient;
            }
        }
    }

    Basically what the TestContainerBuilder does is delegating the service registrations to the inner ServiceCollection which comes from the Microsoft DI Framework. By the way, this is the exact implementation of IContainerBuilder we use in Web API OData v6.x :-)

    Of course, the APIs of IContainerBuilder are kind of “primitive” thus they are not so convenient when directly used to register services. That’s what we are going to address in the next section.

    Register the Required Services

    Once you have your container builder, the next step is to register the required services into the container. We defined many extension methods in ContainerBuilderExtensions to IContainerBuilder to ease the service registration. Below are the signatures of the extension methods and their corresponding examples.

    public static class ContainerBuilderExtensions
    {
        // Examples:
        //   builder.AddService<ITestService, TestService>(ServiceLifetime.Singleton);
        //   builder.AddService<TestService, DerivedTestService>(ServiceLifetime.Scoped);
        //
        // The following will NOT work. TImplementation MUST be a concrete class:
        //   builder.AddService<ITestService, ITestService>(ServiceLifetime.Scoped);
        public static IContainerBuilder AddService<TService, TImplementation>(
            this IContainerBuilder builder,
            ServiceLifetime lifetime)
            where TService : class
            where TImplementation : class, TService
    
        // Examples:
        //   builder.AddService(ServiceLifetime.Transient, typeof(TestService));
        //
        // The following will NOT work. serviceType MUST be a concret class:
        //   builder.AddService(ServiceLifetime.Transient, typeof(ITestService));
        public static IContainerBuilder AddService(
            this IContainerBuilder builder,
            ServiceLifetime lifetime,
            Type serviceType);
    
        // Examples:
        //   builder.AddService<TestService>(ServiceLifetime.Transient);
        // which is equivalent to:
        //   builder.AddService<TestService, TestService>(ServiceLifetime.Transient);
        //
        // The following will NOT work. TService MUST be a concret class:
        //   builder.AddService<ITestService>(ServiceLifetime.Transient);
        public static IContainerBuilder AddService<TService>(
            this IContainerBuilder builder,
            ServiceLifetime lifetime)
            where TService : class
    
        // Examples:
        //   builder.AddService(ServiceLifetime.Singleton, sp => TestService.Instance);
        //   builder.AddService<ITestService>(ServiceLifetime.Scoped, sp => new TestService());
        //   builder.AddService(ServiceLifetime.Singleton, sp => new TestService(sp.GetRequiredService<DependentService>()));
        public static IContainerBuilder AddService<TService>(
            this IContainerBuilder builder,
            ServiceLifetime lifetime,
            Func<IServiceProvider, TService> implementationFactory)
            where TService : class
    
        // Examples (currently we only support the following service prototypes):
        //   builder.AddServicePrototype(new ODataMessageReaderSettings { ... });
        //   builder.AddServicePrototype(new ODataMessageWriterSettings { ... });
        //   builder.AddServicePrototype(new ODataSimplifiedOptions { ... });
        public static IContainerBuilder AddServicePrototype<TService>(
            this IContainerBuilder builder,
            TService instance);
    
        // Examples:
        //   builder.AddDefaultODataServices();
        public static IContainerBuilder AddDefaultODataServices(this IContainerBuilder builder);
    }

    For the usage of the AddService overloads, please see the comments for examples. For AddServicePrototype, we currently only support the following service types: ODataMessageReaderSettings, ODataMessageWriterSettings and ODataSimplifiedOptions. This design follows the Prototype Pattern where you can register a globally singleton instance (as the prototype) for each service type then you will get an individual clone per scope/request. Modifying that clone will not affect the singleton instance as well as the subsequent clones. That is to say now you don’t need to clone a writer setting before editing it with the request-related information just feel safe to modify it for any specific request.

    The AddDefaultODataServices method registers a set of service types with default implementations that come from ODataLib. Typically you MUST call this metod first on your container builder before registering any custom service. Please note that the order of registration matters! ODataLib will always use the last service implementation registered for a specific service type.

    Currently the default services provided by ODataLib and expected to be overrided by users are:

    Service Default Implementation Lifetime Prototype?
    IJsonReaderFactory DefaultJsonReaderFactory Singleton N
    IJsonWriterFactory DefaultJsonWriterFactory Singleton N
    ODataMediaTypeResolver ODataMediaTypeResolver Singleton N
    ODataMessageReaderSettings ODataMessageReaderSettings Scoped Y
    ODataMessageWriterSettings ODataMessageWriterSettings Scoped Y
    ODataPayloadValueConverter ODataPayloadValueConverter Singleton N
    IEdmModel EdmCoreModel.Instance Singleton N
    ODataUriResolver ODataUriResolver Singleton N
    UriPathParser UriPathParser Scoped N
    ODataSimplifiedOptions ODataSimplifiedOptions Scoped Y


    Build and Use the Container in ODataLib

    After you have registered all the required services into the container builder, you can finally build a container from it by calling BuildContainer on your container builder. You will then get a container instance that implements IServiceProvider.

    In order for ODataLib to use the registered services, the container must be passed into ODataLib through some entry points. Currently entry points in ODataLib are ODataMessageReader, ODataMessageWriter and ODataUriParser, which will be covered in the next two subsections.

    Part I: Serialization and Deserialization

    The way of passing container into ODataMessageReader and ODataMessageWriter is exactly the same which is through request and response messages. We are still using the interfaces IODataRequestMessage and IODataResponseMessage but now the actual implementation class (e.g., ODataMessageWrapper in Web API OData) must also implement IContainerProvider. Below is an excerpt of the ODataMessageWrapper class as an example if you are building an OData service directly using ODataLib.

    class ODataMessageWrapper : IODataRequestMessage, IODataResponseMessage, IContainerProvider, ...
    {
        ...
        public IServiceProvider Container { get; set; }
        ...
    }
    
    // Use ODataMessageWrapper to pass the container into ODataLib.
    // The request container will be automatically used in ODataLib.
    ODataMessageWrapper responseMessage = new ODataMessageWrapper();
    responseMessage.Container = Request.GetRequestContainer();
    ODataMessageWriter writer = new ODataMessageWriter(responseMessage);
    // Use the writer to write the response payload.

    After that, the container will be stored in the Container properties of ODataMessageInfo, ODataInputContext and ODataOutputContext (and their subclasses). If you are implementing a custom media type (like Avro, VCard, etc.), you can access the container through those properties. This is a very advanced and complicated scenario thus we will omit the sample here for now.

    If you fail to set the Container in IContainerProvider, it will remain null. In this case, ODataLib will not fail internally but all services will have their default implementations and there would be NO way to replace them with custom ones. That said, if you want extensibility, please use DI :-)

    Part II: URI Parsing

    The way of passing container into URI parsers is a little bit different. You must use the constructor overloads (see below) of ODataUriParser that take a parameter container of IServiceProvider to do so. Using the other constructors will otherwise disable the DI support in URI parsers.

    public sealed class ODataUriParser
    {
        ...
        public ODataUriParser(IEdmModel model, Uri serviceRoot, Uri uri, IServiceProvider container);
        ...
        public ODataUriParser(IEdmModel model, Uri relativeUri, IServiceProvider container);
        ...
    }

    Then the container will be stored in ODataUriParserConfiguration and used in URI parsers. Currently ODataUriResolver, UriPathParser and ODataSimplifiedOptions can be overrided and will affect the behavior of URI parsers.

    Design ODataLib Features for DI

    In the future, we may encounter the need in ODataLib to either move existing classes into DI container, or design new classes that work with DI. Based on the past experience about incorporating DI into ODataLib, here are some tips:

    • Eliminate constructor parameters that are of primitive types because they CANNOT be injected. If they have to be there anyway, consider injecting a factory class instead of the class itself (e.g, IJsonReader and IJsonReaderFactory).
    • Move those types (they are called dependencies) of the remaining constructor parameters into DI container (if they are not in it already) so that they can be injected by the DI framework automatically. If some types cannot be placed in DI container anyway, consider converting the constructors parameters of those types to class properties and using property assignment during initialization.
    • Of course it’s best to use empty constructors.
    • Carefully consider the lifetime of the service. We rarely use Transient as it will degrade the runtime performance of GC. If you want that service to have an individual instance per request, use Scoped. If only one instance is required during the application lifecycle, use Singleton. Please also pay attention to the lifetime of your dependencies!
    • Add the service into AddDefaultODataServices of the ContainerBuilderExtensions class.


    Adapt to Breaking Changes for DI

    After upgrading to ODataLib v7.x, you might find that some parameters or properties in public APIs are missing. Don’t panic! Mostly you will find it in the list (see above) of the services registered in the container. And you will also find the request container in the context. Then it’s very easy to access the missing objects by calling IServiceProvider.GetService. Sometimes retrieving a service every time from the container might look like a performance concern though the actual cost of the DI framework is typically very low (for example, the MS DI Framework uses compiled lambda to optimize for performance). In this case, you might want to cache it in some place but please be cautious that inproper caching may break the lifetime policy of the services.

2. EDMLIB

  • 2.1 Build a basic model

    The EDM (Entity Data Model) library (abbr. EdmLib) primarily contains APIs to build an entity data model that conforms to CSDL (Common Schema Definition Language), and to read/write an entity data model from/to a CSDL document.

    This section shows how to build a basic entity data model using EdmLib APIs.

    Software used in this tutorial

    Create a Visual Studio project

    In Visual Studio, from the File menu, select New > Project.

    Expand Installed > Templates > Visual C# > Windows Desktop, and select the Console Application template. Name the project EdmLibSample, and click OK.

    Install the EdmLib package

    From the Tools menu, select NuGet Package Manager > Package Manager Console. In the Package Manager Console window, type:

    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 directives to introduce the EDM definitions:

    using Microsoft.OData.Edm;

    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 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 EdmEnumMemberValue(1L));
                _categoryType.AddMember("Dresses", new EdmEnumMemberValue(2L));
                _categoryType.AddMember("Sports", new EdmEnumMemberValue(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 that multiple enumeration members (enumerators) can be selected simultaneously;
    • Adds three enumerators 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 within the namespace Sample.NS;
    • Adds the container to the model.

    Note that each model MUST define exactly one entity container (aka. the default entity container) which can be referenced later via the _model.EntityContainer property.

    Add an entity set Customers

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

    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.

    Using factory APIs for EdmModel

    In the above sections, to construct entity/complex types and entity containers, one has to first explicitly instantiate the corresponding CLR types, and then invoke EdmModel.AddElement() to add them to the model. In this version, a new set of factory APIs are introduced that combine the two steps into one, leading to more succinct and cleaner user code. These APIs are defined as extension methods to EdmModel as follows:

    namespace Microsoft.OData.Edm
    {
        public static class ExtensionMethods
        {
            public static EdmComplexType AddComplexType(this EdmModel model, string namespaceName, string name);
            public static EdmComplexType AddComplexType(this EdmModel model, string namespaceName, string name, IEdmComplexType baseType);
            public static EdmComplexType AddComplexType(this EdmModel model, string namespaceName, string name, IEdmComplexType baseType, bool isAbstract);
            public static EdmComplexType AddComplexType(this EdmModel model, string namespaceName, string name, IEdmComplexType baseType, bool isAbstract, bool isOpen);
    
            public static EdmEntityType AddEntityType(this EdmModel model, string namespaceName, string name);
            public static EdmEntityType AddEntityType(this EdmModel model, string namespaceName, string name, IEdmEntityType baseType);
            public static EdmEntityType AddEntityType(this EdmModel model, string namespaceName, string name, IEdmEntityType baseType, bool isAbstract, bool isOpen);
            public static EdmEntityType AddEntityType(this EdmModel model, string namespaceName, string name, IEdmEntityType baseType, bool isAbstract, bool isOpen, bool hasStream);
    
            public static EdmEntityContainer AddEntityContainer(this EdmModel model, string namespaceName, string name);
        }
    }

    So, for example, instead of

    var entityType = new EdmEntityType("NS", "Entity");
    // Do something with entityType.
    model.AddElement(entityType);

    you could simply

    var entityType = model.AddEntityType("NS", "Entity");
    // Do something with entityType.

    Besides being more convenient, these factory APIs are potentially more efficient as advanced techniques may have been employed in their implementations for optimized object creation and handling.

    Write the model to a CSDL document

    Congratulations! You now have a working entity data model! In order to show the model in an intuitive way, we now write it to a CSDL document.

    In the Program.cs file, add the following using directives:

    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;
                    CsdlWriter.TryWriteCsdl(model, writer, CsdlTarget.OData, out errors);
                }
            }
        }
    }

    For now, there is no need to understand how the model is written to CSDL. The details will be explained in the following section.

    Run the sample

    From the DEBUG menu, click Start Debugging to build and run the sample. The console window should appear and then disappear in a flash.

    Open the csdl.xml file under the output directory with Internet Explorer (or any other XML viewer you prefer). The content should look similar to the following:

    As you can see, the document contains all the elements we have built so far.

    References

    [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 use and extend the sample from the previous section.

    Using the CsdlWriter APIs

    We have already used one of the CsdlWriter APIs to write the model to a CSDL document in the previous section.

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

    The CsdlWriter.TryWriteCsdl() method is prototyped as:

    namespace Microsoft.OData.Edm.Csdl
    {
        public class CsdlWriter
        {
            ...
            public static bool TryWriteCsdl(
                IEdmModel model,
                XmlWriter writer,
                CsdlTarget target,
                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 statement on an XmlWriter instance or explicitly call XmlWriter.Flush() (or XmlWriter.Close()) to flush the buffer to its underlying stream. The third parameter target specifies the target implementation of the CSDL being generated, which can be either CsdlTarget.EntityFramework or CsdlTarget.OData. The 4th parameter errors is used to pass out the errors encountered in writing the model. If the method returns true (indicate success), the errors should be an empty Enumerable; otherwise it contains all the errors encountered.

    Using the CsdlReader APIs

    The simplest CsdlReader API is prototyped as:

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

    The first parameter reader takes an XmlReader that reads a CSDL document. The second parameter model passes out the parsed model. The third parameter errors passes out the errors encountered in parsing the CSDL document. If the return value of this method is true (indicate success), the errors should be an empty Enumerable; otherwise it will contain all the errors encountered.

    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(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 the file csdl.xml and the file csdl1.xml under the output directory. The content of csdl1.xml should look like the following:

    You can see that the contents of csdl.xml and csdl1.xml are exactly the same except for the order of the elements. This is because EdmLib will reorder the elements when parsing a CSDL document.

    References

    [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 previous sections. EdmLib supports adding navigation properties targeting an entity set in the entity container or a contained entity set belonging to a navigation property.

    This section shows how to define navigation properties using EdmLib APIs. We will use and extend the sample from the previous section.

    Add a navigation property Friends

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

    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 but targets one or more Customer entities in the entity set Customers;
    • Sets the TargetMultiplicity property to EdmMultiplicity.Many, indicating that one customer can have many orders. Other possible values include ZeroOrOne and One.

    Add entity Type Order and 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, isNullable: false));
                _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
        {
            ...
    #region !!!INSERT THE CODE BELOW!!!
            private EdmNavigationProperty _purchasesProperty;
            private EdmNavigationProperty _intentionsProperty;
    #endregion
            ...
            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 navigation property targeting one or more settled orders in the entity set Orders;
    • Adds an Intentions navigation property targeting a contained entity set of unsettled orders that are not listed in the entity set Orders.

    Run the sample

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

    References

    [Tutorial & Sample] Containment is Coming with OData V4.

  • 2.4 Define singletons

    Defining a singleton in the entity container shares the same simple way as defining an entity set.

    This section shows how to define singletons using EdmLib APIs. We will use and extend the sample from the previous section.

    Add a singleton VipCustomer

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

    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 file csdl.xml under the output directory. The content should look like the following:

    References

    [Tutorial & Sample] Use Singleton to define your special entity.

  • 2.5 Define type inheritance

    Type inheritance means defining a type by deriving from another type. EdmLib supports defining both derived entity types and derived complex types. Adding a derived entity (complex) type is almost identical to adding a normal entity (complex) type except that an additional base type needs to be specified.

    This section shows how to define derived entity (complex) types using EdmLib APIs. We will use and extend the sample from the previous section.

    Add derived entity type UrgentOrder

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

    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 the 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 derived entity type to the entity data model.

    Add derived complex type WorkAddress

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

    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 the 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 derived complex type to the entity data model.

    Run the sample

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

  • 2.6 Define operations

    EdmLib supports defining all types of operations (actions and functions) and operation imports (action imports or function imports). Putting aide the conceptual differences between actions and functions, the way to define them could actually be shared between actions and functions.

    This section shows how to define operations and operation imports using EdmLib APIs. We will use and extend the sample from the previous section.

    Add bound action Rate

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

    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, which has no return value;
    • Adds a binding parameter customer of type Sample.NS.Customer;
    • Adds a parameter rating of type Edm.Int32;
    • Adds the bound action to the model.

    Add an unbound function MostExpensive

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

    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 parameterless composable function MostExpensive within the namespace Sample.NS;
    • Adds the function to the model.

    Add function import MostValuable

    In the SampleModelBuilder.cs file, 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 EdmPathExpression("Orders"));
                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 to the default container;
    • Have the function import return a Sample.NS.Order entity from and only from 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 any valid name).

    Run the sample

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

  • 2.7 Define annotations

    EdmLib supports adding annotations on various model elements, including entity sets, entity types, properties, and so on. Annotations can be put under the Annotations XML element, or under the annotated target model elements (inline annotations). Users can specify the serialization location using the EdmLib API.

    This section shows how to define annotations using EdmLib APIs. We will use and extend the sample from the previous section.

    Add an annotation to entity set Customers

    In the SampleModelBuilder.cs file, add the following using directive:

    using Microsoft.OData.Edm.Csdl;
    using Microsoft.OData.Edm.Vocabularies;

    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 EdmVocabularyAnnotation(_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 to the entity set Customers, which is put under 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 EdmVocabularyAnnotation(_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 EdmVocabularyAnnotation(_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 to the property Customer.Name.

    Run the sample

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

  • 2.8 Using model utilities

    The model utilities include many useful extension methods to various EDM classes and interfaces (e.g., IEdmModel, IEdmType, …). The extension methods are intended to implement some commonly reusable logic to simplify model manipulations. These methods can be roughly classified into five categories:

    • Searching. The naming convention is Find<ElementName> (e.g., IEdmModel.FindDeclaredEntitySet());
    • Predicate. The naming convention is Is<ElementName> (e.g., IEdmOperation.IsFunction());
    • Information. The naming convention is <InformationName> (e.g., IEdmNavigationSource.EntityType());
    • Getter. The naming convention is Get<Name> (e.g., IEdmModel.GetTermValue());
    • Setter. The naming convention is Set<Name> (e.g., IEdmModel.SetEdmVersion()).

    The most widely used parts are Searching, Predicate, and Information. Extension methods in the latter two parts are trivial, because they work literally as their names suggest. This section focuses on Searching. We will use and extend the sample from the previous section.

    Exercise model utility APIs

    In the Program.cs file, add the using directive

    using System.Linq;

    and 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 = (IEdmVocabularyAnnotation)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 on the console window should look like the following:

  • 2.9 Model references

    Model referencing is an advanced OData feature. When you want to use types defined in another model, you can reference that model in your own model. Typically when talking about model referencing, there is a main model and one or more sub-models. The main model references the sub-models. The role a particular model plays is not fixed, for a main model may also be referenced by another model. That is, models can be mutually referenced.

    This section covers a scenario where we have one main model and two sub-models. The main model references the two sub-models, while the two sub-models references each other. We will introduce two ways to define model references: by code and by CSDL. If you would like to create the model by writing code, you can take a look at the first part of this section. If you want to create your model by reading a CSDL document, please refer to the second part.

    Define model references by code

    Let’s begin by defining the first sub-model subModel1. The model contains a complex type NS1.Complex1 which contains a structural property of another complex type defined in another model. We also add a model reference to subModel1 pointing to the second model located at http://model2. The 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(new Uri("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(new Uri("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 point 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 that 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 CSDL

    As an example, we store the CSDL 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 (!CsdlReader.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 model constructed in either way should be the same.

  • 2.10 Define referential constraints

    Referential constraints ensure that entities being referenced (principal entities) always exist. In OData, having one or more referential constraints defined for a partner navigation property on a dependent entity type also enables users to address the related dependent entities from principal entities using shortened key predicates (see [OData-URL]). A referential constraint in OData consists of one principal property (the ID property of the entity being referenced) and one dependent property (the ID property to reference another entity). This section shows how to define referential constraints on a partner navigation property.

    Sample

    Create an entity type Test.Customer with a key property id of type 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 type Edm.String.

    var order = new EdmEntityType("Test", "Order", null, false, true);
    var orderCustomerId = order.AddStructuralProperty("customerId", EdmPrimitiveTypeKind.String, false);
    var orderOrderId = order.AddStructuralProperty("orderId", EdmPrimitiveTypeKind.String, false);
    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 type Edm.String, orderId of type Edm.String, and id of type Edm.Int32.

    var detail = new EdmEntityType("Test", "Detail");
    var detailCustomerId = detail.AddStructuralProperty("customerId", EdmPrimitiveTypeKind.String, false);
    var detailOrderId = detail.AddStructuralProperty("orderId", EdmPrimitiveTypeKind.String, false);
    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 order shown.

    Then you can query the details using either full key predicate

    http://host/customers('customerId')/orders(customerId='customerId',orderId='orderId')/Test.DetailedOrder/details(customerId='customerId',orderId='orderId',id=1)
    

    or shortened key predicate

    http://host/customers('customerId')/orders('orderId')/Test.DetailedOrder/details(1)
    

    Key-as-segment convention is also supported

    http://host/customers/customerId/orders/orderId/Test.DetailedOrder/details/1
    
  • 2.11 Specify type facets for type definitions

    Users can specify various type facets for references to type definitions. The only constraint is that the type facets specified should be applicable to the underlying type definition.

    If you want to specify type facets, you need to call this constructor:

    public EdmTypeDefinitionReference(
        IEdmTypeDefinition typeDefinition,
        bool isNullable,
        bool isUnbounded,
        int? maxLength,
        bool? isUnicode,
        int? precision,
        int? scale,
        int? spatialReferenceIdentifier);

    Here is sample code to create an EDM type definition reference with type facets

    IEdmTypeDefinition typeDefinition = new EdmTypeDefinition("MyNS", "Title", EdmPrimitiveTypeKind.String);
    IEdmTypeDefinitionReference reference = new EdmTypeDefinitionReference(
        typeDefinition,
        isNullable: true,
        isUnbounded: false,
        maxLength: 10,
        isUnicode: true,
        precision: null,
        scale: null,
        spatialReferenceIdentifier: null);
    • isNullable: true to allow null values; false otherwise.
    • isUnbounded: true to indicate MaxLength="max"; false to indicate that the MaxLength is a bounded value.
    • maxLength: null for unspecified; other values for specified lengths. Invalid if isUnbounded is true.
    • isUnicode: true if the encoding is Unicode; false for non-Unicode encoding; null for unspecified.
    • precision: null for unspecified; other values for specified precisions; MUST be non-negative.
    • scale: null to indicate Scale="variable"; other values for specified scales; MUST be non-negative.
    • spatialReferenceIdentifier: null to indicate SRID="variable"; other values for specified SRIDs; MUST be non-negative.

    It’s worth mentioning that if you call the overloaded constructor taking two parameters, all the type facets will be auto-computed according to the OData protocol. For example, the default value of SRID is 0 for geometry types and 4326 for geography types.

    public EdmTypeDefinitionReference(
        IEdmTypeDefinition typeDefinition,
        bool isNullable);
  • 2.12 Other topics

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()
            {
                _model = new EdmModel();
                var _addressType = new EdmComplexType("test", "Address");
                _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 ODataResource for the Address type could be constructed as follows:

    var addressValue = new ODataResource
    {
        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. RELEASE NOTES

  • ODataLib 7.0.0

    To briefly summarize the breaking changes, most of them fall into one of four categories:

    Improved Performance

    We will get better writer performance across the board.

    Introducing Dependency Injection

    This feature will substantially increase extensibility, like allowing customers to replace entire components such as the UriPathParser with their own implementation. Introducing DI make it much easier to use the same reader/writer settings across the board.

    Removed Legacy Code

    There was a lot of vestigial code left around from the OData v1-3 days that we’ve removed.

    Improved API Design

    Most of our API improvements fall into the category of namespace simplifications or updating verbiage. The single most impactful change that we made was deciding to merge entity type and complex type in ODataLib. We did this because complex type and entity type are becoming more and more similar in the protocol, but we continue to pay overhead to make things work for both of them.

    Changes in ODataLib 7.0 Release

    New Features

    [Issue #245] Support duplicate non-OData query options.

    [Issue #248] Support untyped JSON.

    • This feature will allow customers to extend their OData payloads with arbitrary JSON. In the extreme, it’s theoretically possible for the whole response to be untyped.

    [Issue #271] Support writing relative URIs in OData batch operations.

    • Add an enum type BatchPayloadUriOption to support three URI formats in batch payload.

    [Issue #366] Support collection of mixed primitive types and Edm.Untyped.

    [Issue #501] Integrate dependency injection (DI).

    • We introduced an abstraction layer consisting of two interfaces IContainerBuilder and IContainerProvider so that ODataLib is decoupled from any concrete implementation of DI framework. Now we support the following services to be replaced by users:

      • JSON reader via IJsonReaderFactory
      • JSON writer via IJsonWriterFactory
      • ODataMediaTypeResolver
      • ODataPayloadValueConverter
      • ODataUriResolver
      • UriPathParser
    • We also support prototype services. For each prototype service, you can specify a globally singleton prototype instance. Then for each request, a cloned instance will be created from that prototype which can isolate the modification within the request boundary. Currently we support three prototype services:

      • ODataMessageReaderSettings
      • ODataMessageWriterSettings
      • ODataSimplifiedOptions

    [Issue #502] Support URI path syntax customization.

    • Expose UriPathParser.ParsePathIntoSegments to support customizing how to separate a Uri into segments
    • Provide ParseDynamicPathSegmentFunc for customizing how to parse a dynamic path segment.

    [Issue #613] Support type facets when referencing TypeDefinition types.

    [Issue #622] Support navigation property on complex types.

    • EdmLib supports adding navigation property on complex type in model.
    • ODataUriParser support parsing related Uri path or query expressions.
    • ODataLib support reading and writing navigation properties on complex type.

    [Issue #629] Support multi-NavigationPropertyBindings for a single navigation property by using different paths

    • Navigation property used in multi bindings with different path is supported for navigation under containment and complex.

    [Issue #631] Support fluent writer API.

    • In previous version, paired WriteStart and WriteEnd calls are used in writing payloads. This syntax is error-prone, and soon gets unmanageable with complex and deeply nested payloads. In this new release, you can instead write payloads using the neat fluent syntax.

    [Issue #635] Support collection of nullable values for dynamic properties.

    [Issue #637] Support system query options without $ prefix.

    • Expose ODataUriParser.EnableNoDollarQueryOptions flag to enable user to support ‘$’ system query prefix optional.

    Add ODataSimplifiedOptions class for simplified reader, writer, URL parsing options.

    Support duplicate custom instance annotations.

    Fixed Bugs

    [Issue #104] Function imports with parameters are included in service document.

    [Issue #498] Typos in namespace/class names.

    [Issue #508] YYYY-MM-DD in URI should be parsed into Date, not DataTimeOffset.

    [Issue #556] URI template parser doesn’t work correctly if key is of an enum type.

    [Issue #573] CollectionCount is not publicly accessible.

    [Issue #592] ODataUriParser.ParsePath() doesn’t work correctly if EntitySetPath is not specified in model.

    [Issue #628] Dynamic complex (collection) property is annotated with association and navigation links.

    [Issue #638] Null value at the first position in a complex collection cannot be read.

    [Issue #658] ODataValueUtils.ToODataValue doesn’t work with System.Enum objects.

    Improvements

    Legacy Code Clean-up

    [Issue #385] Remove junk code to improve stability and reduce assembly size.

    • Deprecated build constants
    • Code in ODataLib that has duplication in EdmLib
    • Platform-dependent code (e.g., redefinition of TypeCode and BindingFlags, Silverlight code, etc.)
    • Deprecated classes like EntitySetNode

    [Issue #500] Remove deprecated NuGet profiles

    • Removed support for:
      • Desktop and Profile328 for .NET 4.0
      • Profile259 for .NET 4.5
      • dnxcore50 and dnx451 for ASP.NET 5.0 (deprecated)
    • What we have now:
      • Profile111 for .NET 4.5 which is compatible with most .NET 4.5+ applications, UWP, Xamarin/Mono, etc.
      • .NET 3.5 for Office (will not be publicly available)

    [Issue #510] Remove Atom support

    [Issue #511] Clean property and method in ODataMessageReaderSettings and ODataMessageWriterSettings

    • Remove API to enable default, service and client settings
    • Move ODataSimplified and UseKeyAsSegment to ODataSimplifiedOptions
    • Rename DisableXXX to EnableXXX
    • Remove base class of reader and writer settings

    [Issue #548] Rename “value term” and “value annotation” in EdmLib.

    • “Value term” and “value annotation” are concepts of ODataV3, In ODataV4 we remove/rename obsolete interfaces and merge some interfaces with their base interfaces so as to make APIs clearer and more compact.

    [Issue #565] Update CoreVocabularies.xml to the new version and related APIs.

    [Issue #564] Remove Edm.ConcurrencyMode attribute from Property.

    [Issue #606] Remove V3 vocabulary expressions

    • Interfaces removed: IEdmEnumMemberReferenceExpression, IEdmEnumMemberReferenceExpression, IEdmEntitySetReferenceExpression, IEdmPropertyReferenceExpression, IEdmParameterReferenceExpression and IEdmOperationReferenceExpression
    • For the previous IEdmEntitySetReferenceExpression, please use IEdmPathExpression where users need to provide a path (a list of strings) to a navigation property with which we can resolve the target entity set from the navigation source.
    • For the previous IEdmOperationReferenceExpression, please use IEdmFunction because the Edm.Apply only accepts an EDM function in OData V4. This also simplifies the structure of Edm.Apply expression.

    [Issue #618] Remove deprecated validation rules

    • ComplexTypeInvalidAbstractComplexType
    • ComplexTypeInvalidPolymorphicComplexType
    • ComplexTypeMustContainProperties
    • OnlyEntityTypesCanBeOpen

    Public API Simplification

    [Issue #504] Unify entity and complex (collection) type serialization/deserialization API.

    • Merge the reading/writing behavior for complex and entity, collection of complex and collection of entity.
      • Change ODataEntry to ODataResource and remove ODataComplexValue to support complex and entity.
      • Change ODataFeed to ODataResourceSet to support feed and complex collection.
      • Change ODataNavigationLink to ODataNestedResourceInfo to support navigation property and complex property or complex collection property.
      • Change reader/writer APIs to support IEdmStructuredType.
    • We don’t merge entity type/complex type in EdmLib since they are OData concepts.

    [Issue #491] Simplified namespaces.

    [Issue #517] Centralized reader/writer validation. [Breaking Changes]

    • Add an enum ValidationKinds to represent all validation kinds in reader and writer.
    • Add Validations property in ODataMessageWriterSettings/ODataMessageReaderSettings to control validations.
    • Remove some APIs.

    [Issue #571] Rename ODataUrlConvention to ODataUrlKeyDelimiter

    • Rename ODataUrlConvention to ODataUrlKeyDelimiter.
    • Use ODataUrlKeyDelimiter.Slash instead of ODataUrlConvention.Simplified or ODataUrlConvention.KeyAsSegment
    • Use ODataUrlKeyDelimiter.Parentheses instead of ODataUrlConvention.Default

    [Issue #614] Improve API design around ODataAnnotatable

    • Merge SerializationTypeNameAnnotation into ODataTypeAnnotation
    • Refactor ODataTypeAnnotation to contain only the type name
    • Remove unnecessary inheritance from ODataAnnotatable in URI parser and simplify the API of ODataAnnotatble
    • GetAnnotation<T>() and SetAnnotation<T>() will no longer be available because ODataAnnotatable should only be responsible for managing instance annotations.

    Public API Enhancement

    [Issue #484] Preference header extensibility

    • Public class HttpHeaderValueElement, which represents http header value element
    • Remove sealed from public class ODataPreferenceHeader

    [Issue #544] Change Enum member value type from IEdmPrimitiveValue to a more specific type.

    • Add interface IEdmEnumMemberValue and class EdmEnumMemberValue to represent enum member value specifically. AddMember() under EnumType now accepts IEdmEnumMemberValue instead of IEdmPrimitiveValue as member value.

    [Issue #621] Make reader able to read contained entity/entityset without context URL.

    [Issue #640] More sensible type, namely IEnumerable, for ODataCollectionValue.Items.

    [Issue #643] Adjust query node kinds in Uri Parser in order to support navigation under complex. [Breaking Changes]

    Improved standard-compliance by forbidding duplicate property names.

    Writer throws more accurate and descriptive exceptions.

    Other Improvements

    [Issue #493] Replace ThrowOnUndeclaredProperty with the more accurate ThrowOnUndeclaredPropertyForNonOpenType.

    [Issue #551] Change the type of EdmReference.Uri to System.Uri.

    [Issue #558, [Issue #611] Improve writer performance. Up to 25% improvements compared to ODL 6.15 are achieved depending on scenario.

    [Issue #632] Rename CsdlXXX to SchemaXXX, and EdmxXXX to CsdlXXX.

    The original naming is confusing. According to the CSDL spec:

    An XML document using these namespaces and having an edmx:Edmx root element will be called a CSDL document.

    So, Edmx is only part of a valid CSDL document. In previous version, CsdlReader is actually unable to read a CSDL document. It’s only able to read the Schema part of it. EdmxReader, on the other hand, is able to read a whole CSDL document. To clear up the concepts, the following renaming has been done:

    1. CsdlReader/Writer to SchemaReader/Writer;
    2. EdmxReader/Writer to CsdlReader/Writer;
    3. EdmxReaderSettings to CsdlReaderSettings;
    4. EdmxTarget to CsdlTarget.

    Notes

    This release delivers OData core libraries including ODataLib, EdmLib and Spatial. OData Client for .NET is not published in this release.

  • Breaking changes about Query Nodes

    The expression of $filter and $orderby will be parsed to multiple query nodes. Each node has particular representation, for example, a navigation property access will be interpreted as SingleNavigationNode and collection of navigation property access will be interpreted as CollectionNavigationNode.

    Since we have merged complex type and entity type in OData Core lib, complex have more similarity with entity other than primitive property. Also in order to support navigation property under complex, the query nodes’ type and hierarchy are changed and adjusted to make it more reasonable.

    Nodes Change

    Nodes Added

    SingleComplexNode
    CollectionComplexNode
    

    Nodes Renamed

    Old New
    NonentityRangeVariable NonResourceRangeVariable
    EntityRangeVariable ResourceRangeVariable
    NonentityRangeVariableReferenceNode NonResourceRangeVariableReferenceNode
    EntityRangeVariableReferenceNode ResourceRangeVariableReferenceNode
    EntityCollectionCastNode CollectionResourceCastNode
    EntityCollectionFunctionCallNode CollectionResourceFunctionCallNode
    SingleEntityCastNode SingleResourceCastNode
    SingleEntityFunctionCallNode SingleResourceFunctionCallNode

    Nodes Removed

    CollectionPropertyCast
    SingleValueCast
    

    API Change

    1. Add SingleResourceNode as the base class of SingleEntityNode and SingleComplexNode

    2. SingleNavigationNode and CollectionNavigationNode accepts SingleResourceNode as parent node and also accepts bindingpath in the constructor.The parameter order is also adjusted.

      Take SingleNavigationNode for example:

    public SingleNavigationNode(IEdmNavigationProperty navigationProperty, SingleEntityNode source)

    Changed to:

    public SingleNavigationNode(SingleResourceNode source, IEdmNavigationProperty navigationProperty, IEdmPathExpression bindingPath)

    Behavior Change for complex type nodes

    Complex property used to share nodes with primitive property, now it shares most nodes with entity. Here lists the nodes that complex used before and now.

    Before Now
    NonentityRangeVariable ResourceRangeVariable
    NonentityRangeVariableReference ResourceRangeVariableReference
    SingleValuePropertyAccessNode SingleComplexNode
    SingleValueCastNode SingleResourceCastNode
    SingleValueFunctionCallNode SingleResourceFunctionCallNode
    CollectionPropertyAccessNode CollectionComplexNode
    CollectionPropertyCastNode CollectionResourceCastNode
    CollectionFunctionCallNode CollectionResourceFunctionCallNode
  • Breaking changes about validation settings

    We used to have lots of validation related members/flags in ODataMessageReaderSettings and ODataMessageWriterSettings. In OData 7.0, we cleaned up the out-dated flags and put the remained flags together and keep them be considered in a consistent way.

    Removed APIs

    ODataMessageWriterSettings ODataMessageReaderSettings
    EnableFullValidation EnableFullValidation
    EnableDefaultBehavior() EnableDefaultBehavior()
    EnableODataServerBehavior() EnableODataServerBehavior()
    EnableWcfDataServicesClientBehavior() EnableWcfDataServicesClientBehavior()
      UndeclaredPropertyBehaviorKinds
      DisablePrimitiveTypeConversion
      DisableStrictMetadataValidation

    The EnablexxxBehavior() in writer and reader settings actually wrapped few flags.

    ODataWriterBehavior ODataReaderBehavior
    AllowDuplicatePropertyNames AllowDuplicatePropertyNames
    AllowNullValuesForNonNullablePrimitiveTypes  

    Those flags are all removed, and an enum type would represent all the settings instead.

    New API

    A flag enum type ValidationKinds to represent all validation kinds in reader and writer:

    /// <summary>
    /// Validation kinds used in ODataMessageReaderSettings and ODataMessageWriterSettings.
    /// </summary>
    [Flags]
    public enum ValidationKinds
    {
        /// <summary>
        /// No validations.
        /// </summary>
        None = 0,
    
        /// <summary>
        /// Disallow duplicate properties in ODataResource (i.e., properties with the same name).
        /// If no duplication can be guaranteed, this flag can be turned off for better performance.
        /// </summary>
        ThrowOnDuplicatePropertyNames = 1,
    
        /// <summary>
        /// Do not support for undeclared property for non open type.
        /// </summary>
        ThrowOnUndeclaredPropertyForNonOpenType = 2,
    
        /// <summary>
        /// Validates that the type in input must exactly match the model.
        /// If the input can be guaranteed to be valid, this flag can be turned off for better performance.
        /// </summary>
        ThrowIfTypeConflictsWithMetadata = 4,
    
        /// <summary>
        /// Enable all validations.
        /// </summary>
        All = ~0
    }

    Writer: Add member Validations which accepts all the combinations of ValidationKinds

    /// <summary>
    /// Configuration settings for OData message writers.
    /// </summary>
    public sealed class ODataMessageWriterSettings
    {
    
    /// <summary>
    /// Gets or sets Validations in writer.
    /// </summary>
    public ValidationKinds Validations { get; set; }
    
    }

    Reader: Add member Validations which accepts all the combinations of ValidationKinds

    /// <summary>
    /// Configuration settings for OData message readers.
    /// </summary>
    public sealed class ODataMessageReaderSettings
    {
    
    /// <summary>
    /// Gets or sets Validations in reader.
    /// </summary>
    public ValidationKinds Validations { get; set; }
    
    }

    Sample Usage

    writerSettings.Validations = ValidationKinds.All
    Equal to: writerSettings.EnableFullValidation = true

    readerSettings.Validations |= ValidationKinds.ThrowIfTypeConflictsWithMetadata
    Equal to: readerSettings.DisableStrictMetadataValidation = false

    Same for reader.

  • Breaking changes about merge entity and complex

    This page will describes the Public API changes for “Merge entity and complex”. The basic idea is that we named both an entity and a complex instance as an ODataResource, and named a collection of entity or a collection of complex as an ODataResourceSet.

    API Changes

    Following is difference of public Apis between ODataLib 7.0 and ODataLib 6.15.

      ODataLib 6.15 ODataLib 7.0
      ODataEntry ODataResource
      ODataComplexValue ODataResource
      ODataFeed ODataResourceSet
      ODataCollectionValue for Complex ODataResourceSet
      ODataNavigationLink ODataNestedResourceInfo
         
    ODataPayloadKind Entry Resource
      Feed ResourceSet
         
    ODataReaderState EntryStart ResourceStart
      EntryEnd ResourceEnd
      FeedStart ResourceSetStart
      FeedEnd ResourceSetEnd
      NavigationLinkStart NestedResourceInfoStart
      NavigationLinkEnd NestedResourceInfoEnd
         
    ODataParameterReaderState Entry Resource
      Feed ResourceSet
         
    ODataInputContext ODataReader CreateEntryReader (IEdmNavigationSource navigationSource, IEdmEntityType expectedEntityType) ODataReader CreateResourceReader(IEdmNavigationSource navigationSource, IEdmStructuredType expectedResourceType)
      ODataReader CreateFeedReader (IEdmEntitySetBase entitySet, IEdmEntityType expectedBaseEntityType) ODataReader CreateResourceSetReader(IEdmEntitySetBase entitySet, IEdmStructuredType expectedResourceType)
         
    ODataOutputContext ODataWriter CreateODataEntryWriter (IEdmNavigationSource navigationSource, IEdmEntityType entityType) ODataWriter CreateODataResourceWriter(IEdmNavigationSource navigationSource, IEdmStructuredType resourceType)
      ODataWriter CreateODataFeedWriter (IEdmEntitySetBase entitySet, IEdmEntityType entityType) ODataWriter CreateODataResourceSetWriter(IEdmEntitySetBase entitySet, IEdmStructuredType resourceType)
         
    ODataParameterReader ODataReader CreateEntryReader () ODataReader CreateResourceReader ()
      ODataReader CreateFeedReader () ODataReader CreateResourceSetReader ()
         
    ODataParameterWriter ODataWriter CreateEntryWriter (string parameterName) ODataWriter CreateResourceWriter (string parameterName)
      ODataWriter CreateFeedWriter (string parameterName) ODataWriter CreateResourceSetWriter (string parameterName)
         
    ODataMessageReader public Microsoft.OData.Core.ODataReader CreateODataEntryReader () public Microsoft.OData.ODataReader CreateODataResourceReader ()
      public Microsoft.OData.Core.ODataReader CreateODataEntryReader (IEdmEntityType entityType) public Microsoft.OData.ODataReader CreateODataResourceReader (IEdmStructuredType resourceType)
      public Microsoft.OData.Core.ODataReader CreateODataEntryReader (IEdmNavigationSource navigationSource, IEdmEntityType entityType) public Microsoft.OData.ODataReader CreateODataResourceReader (IEdmNavigationSource navigationSource, IEdmStructuredType resourceType)
      public Microsoft.OData.Core.ODataReader CreateODataFeedReader () public Microsoft.OData.ODataReader CreateODataResourceSetReader ()
      public Microsoft.OData.Core.ODataReader CreateODataFeedReader (IEdmEntityType expectedBaseEntityType) public Microsoft.OData.ODataReader CreateODataResourceSetReader (IEdmStructuredType expectedResourceType)
      public Microsoft.OData.Core.ODataReader CreateODataFeedReader (IEdmEntitySetBase entitySet, IEdmEntityType expectedBaseEntityType public Microsoft.OData.ODataReader CreateODataResourceSetReader (IEdmEntitySetBase entitySet, IEdmStructuredType expectedResourceType)
        public ODataReader CreateODataUriParameterResourceSetReader(IEdmEntitySetBase entitySet, IEdmStructuredType expectedResourceType)
        public ODataReader CreateODataUriParameterResourceReader(IEdmNavigationSource navigationSource, IEdmStructuredType expectedResourceType)
         
    ODataMessageWriter public Microsoft.OData.Core.ODataWriter CreateODataEntryWriter () public Microsoft.OData.ODataWriter CreateODataResourceSetWriter ()
      public Microsoft.OData.Core.ODataWriter CreateODataEntryWriter (IEdmNavigationSource navigationSource) public Microsoft.OData.ODataWriter CreateODataResourceSetWriter (IEdmEntitySetBase entitySet)
      public Microsoft.OData.Core.ODataWriter CreateODataEntryWriter (IEdmNavigationSource navigationSource, IEdmEntityType entityType) public Microsoft.OData.ODataWriter CreateODataResourceSetWriter (IEdmEntitySetBase entitySet, IEdmStructuredType resourceType)
      public Microsoft.OData.Core.ODataWriter CreateODataFeedWriter () public Microsoft.OData.ODataWriter CreateODataResourceWriter ()
      public Microsoft.OData.Core.ODataWriter CreateODataFeedWriter (IEdmEntitySetBase entitySet) public Microsoft.OData.ODataWriter CreateODataResourceWriter (IEdmNavigationSource navigationSource)
      public Microsoft.OData.Core.ODataWriter CreateODataFeedWriter (IEdmEntitySetBase entitySet, IEdmEntityType entityType) public Microsoft.OData.ODataWriter CreateODataResourceWriter (IEdmNavigationSource navigationSource, IEdmStructuredType resourceType)
        public ODataWriter CreateODataUriParameterResourceWriter(IEdmNavigationSource navigationSource, IEdmStructuredType resourceType)
        public ODataWriter CreateODataUriParameterResourceSetWriter(IEdmEntitySetBase entitySetBase, IEdmStructuredType resourceType)
  • ODataLib 7.1.0

    Changes in ODataLib 7.1.0 Release

    Migration to .NET Standard 1.1

    [Commit a2af8c6c19f104f46b442df7f57f624ed77d82fc] Update profile111 to .netstandard1.1.

    [Commit 6f746cd0cd9d62a5bc57e345a910a6f9d8d4dc1b] Adding new projects and solution for .NET Standard version of ODL.

    Creating a new solution file for .NET Standard projects. Adding .NET Standard versions of Microsoft.Spatial, Microsoft.OData.Edm, Microsoft.OData.Core, and Microsoft.OData.Client. MSBuild doesn’t pre-process the dependency graph provided by the csproj files, so explicitly spoonfeeding the chain in the Microsoft.Test.OData.DotNetStandard.sln file.

    Update build script to default to VS 2015 for .NET Standard, removing NuGetPackage test project and deprecating it, adding E2E .NET Standard project

    Remove duplicate file: IEdmReferentialConstraint.cs.

    Change version to 7.1.0

    Note: “nuget restore” needs to be run manually on the new project files for them to compile.

    New Features

    [Commit c0c6006a5a8683507c38623144a145de128851c6] Adding support and tests for virtual property count.

    [Commit d064a1c6358130c581bb9d5bb32e774f8e921992] Adding support and tests for custom aggregation methods.

    [Commit 8e965e9f89951dbfea510e67cbbe89e4f75fa69b] Add support for operations with no bindings.

    Fixed Bugs

    [Issue #525] Support for AnnotationPath.

    [Issue #526] Support for IncludeInServiceDocument.

    [Issue #680] Text “Date” will be parsed as “EDM.Date” type segment in URL parser.

    [Issue #687] Null exception when HttpHeaderValueElement.Value is not set.

    [Issue #706] Serialization exception when 2 subtypes define a property with the same name but different types.

    [Issue #758] countdistinct and $count are returning Edm.Int64 which is not spec compliant.

    [Issue #776] Fix ContextUrl generation for operations in path.

    [Issue #777] Fix trailing whitespace on empty line.

    [Issue #778] Fix tests and code for reading nested results from operations.

    Improvements

    [Commit 6e2dee52b37e620926cd0535f40d5537ba839c05] Add Test solutions for WP WindowsStore Portable.

    [Commit d695d6ded6d44fa3fdb7abbd5f8dc19c29330e10] Update license.

    [Commit 8521d38405351f789134d8945a696a18eec929d3] Fix Phone Project.

    [Commit dc9632b686e47dba8a0bbeffb5cc8c5850e27c8b] Fix fxcop issue.

    [Commit 3ea2d70d14c97344f43383d867a9edd81eb407a3] Add test cases for DotNetCore.

    [Commit 2ef3fc921ad4b557ef67cd17ce95eb08b33fa14e] Update nuget.exe to 3.5.0 to resolve build issue with xunit.

    [Commit 0dc70678f05201ebc48c83219f6b4450078d0693] Reordering comments to avoid compiler warnings.

    [Commit bbd9ede1e73de3538af6f1a223bcc7fe689688f4] Update test project to copy the new and old version of the EntityFramework.nuspec file.

    [Commit 6eb8a4b5ecff1d83d883a95162d187ee3a44f935] Remove use of StringBuilder.Clear() for .NET 3.5 support.

    [Commit c1edd714bf58ddd2876e10d31f0bd9ad81e25664] Added metadata to tests for new bound function.

    Notes

    This release delivers OData core libraries including ODataLib, EdmLib and Spatial. OData Client for .NET is not published in this release.

  • ODataLib 7.1.1

    Changes in ODataLib 7.1.1 Release

    Notes

    7.1.1 is a re-release of 7.1.0 without the NetStandard version of ODataLib, EdmLib and Spatial due to an issue found in the NetStandard versions of those binaries. The PCL versions of those binaries are unaffected.

    This release delivers OData core libraries including ODataLib, EdmLib and Spatial. OData Client for .NET is not published in this release.

  • ODataLib 7.2.0

    Changes in ODataLib 7.2.0 Release

    Notes

    7.2.0 re-introduces .NET Standard 1.1 libraries of ODataLib (OData.Core, OData.Edm, and Microsoft.Spatial). The PCL versions remain in the packages and are shipped alongside the new .NET Standard libraries. Bug fixes and additional test validations are also included in this release.

    Features

    [Commit 0b54111ee7909e71263b83fc60268de0de817986] Expose UriQueryExpressionParser.ParseFilter as a public API [#805]

    Fixed Bugs

    [Issue #789] BUG? Exception when create EdmModel V7.1.0

    Improvements

    [Commit 072f6f7c9bc4c739e553f7fa0996618c621a6589] Adding FxCop exclusion for CA3053:UseXmlSecureResolver in code that compiles under .Net portable framework.

    [Commit 272c74afd1dda7a4a8e562c45dd9da9a9d74dd8b] Fix suppression for CA3053 to reference proper category and checkid.

    [Commit 170827a01f9141649a848aefb13531163dedd65e] This change fixes test failures in both local machine and in the lab

    [Commit 66738741dbecad8e0dc32fa787a9f98898a2beda] Adding .NET Core unit tests that integrate .NET Standard Libraries (#803)

    This release delivers OData core libraries including ODataLib, EdmLib and Spatial. OData Client for .NET is not published in this release.

  • ODataLib 7.3.0

    Changes in ODataLib 7.3.0 Release

    Notes

    ODataLib 7.3.0 adds support key features including optional function parameters, parser support for the $compute clause, primitive type casts, and the ability to read and write untyped data as structured values.

    Features

    [[#760] Aggregation not supported for dynamic properties

    [#782] Add support for optional parameters in function

    [#799] Support for $compute in $select and $filter

    [#800] Support for structured reading/writing of untyped values

    [#801] Supporting Primitive Type Casts

    Fixed Bugs

    [Issue #747] Cannot select dynamic property of a dynamic property

    [Issue #814] Support writing enum-valued Annotations

    [Issue #856] Raw Value serializer output json string for Spatial type

    This release delivers OData core libraries including ODataLib, EdmLib and Spatial. OData Client for .NET is not published in this release.

  • ODataLib 7.3.1

    Changes in ODataLib 7.3.1 Release

    Notes

    ODataLib 7.3.1 addresses an issue where $compute parsing was not triggered by a global call to ParseUri.

    Fixed Bugs

    [Issue #927] Add Compute to ParseUri.

    This release delivers OData core libraries including ODataLib, EdmLib and Spatial. OData Client for .NET is not published in this release.

  • ODataLib 7.4.0

    Changes in ODataLib 7.4.0 Release

    Notes

    ODataLib 7.4.0 has the following beta releases:

    ODataLib 7.4.0.beta

    Features

    [Issue #103] OData v4: Deserialize client unknown properties into OData Object Model.

    [Issue #801] Support Primitive Type Casts.

    [Issue #988] Add support for 4.01 Delta Format

    Fixed Bugs

    [Issue #698] DataServiceQuerySingle.GetValueAsync inconsistent with GetValue in support for GET returning 404.

    [Issue #800] Need for parsing Open types using OData.NET v7.X.

    [Issue #949] ODataUriExtensions.BuildUri ignores $skiptoken.

    [PR #961] Add support for ENUM keys.

    [Issue #965] Parsing encoded character with special meaning in ODataJsonLightContextUriParser.

    ODataLib 7.4.0.beta2

    Features

    [Issue #226] Support a json serialization for $batch.

    [Issue #866] “Microsoft.OData.Client” support for ,NET Core.

    Fixed Bugs

    [PR #980] Support enum to string comparision.

    [PR #995] Use ConcurrentDictionary in all platforms to make client edm model thread safe.

    [Issue #1008 & Issue #1009] Fix property validation issue.

    [Issue #1101] Microsoft.OData.Client 7.0+ needs to support GetValue like the older OData Client.

    ODataLib 7.4.0.beta3

    Features

    [PR #1020] DependsOn Ids for Multipart/Mixed Batch

    Fixed Bugs

    [Issue #1022] Calculate correct context URI with Operation path segment.

    [Issue #1193] Add the extension function back to derived class.

    [Issue #1028] Add the enum member expression into validation and don’t return the error message.

    ODataLib 7.4.0 RTM

    Features

    [Issue #1037] Support reading/writing OData 4.01 compatible JSON payloads.


    This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.

  • ODataLib 7.4.1

    Changes in ODataLib 7.4.1 Release

    Notes

    ODataLib 7.4.1 includes the following items: a new OData Client Code Gen extension for VS2017 using the latest version of the libraries, built-in abstract types for Edm models, KeyAsSegmentSupported boolean to the capabilities vocabulary, added validation rules to abstract types, support for AnnotationSegment, NuGet package testing, and various bug fixes.

    Features

    [[#987] Adding new OData Client Code Gen for VS2017

    [[#1042] Remove the NavigationPropertyEntityMustNotIndirectlyContainItself rule

    [[#1051] Add the build-in abstract type into Edm core model - Edm Type Part.

    [[#1055] OptionalDollarSign: Small test update and expose API for DI option setter/getter

    [[#1056] Add KeyAsSegmentSupported annotation term to Capabiliites vocabulary

    [[#1058] Add the validation rules to the abstract types

    [[#1075] Add support for AnnotationSegment to PathSegmentHandler.

    [[#1080] Add nuget package testing.

    Fixed Bugs

    [Issue #530] LINQ query generation with Date functions produces weird urls

    [Issue #1027] Edm.NavigationPropertyPath not supported

    [Issue #1040] Need to update batch changeset ID to boundary value

    [Issue #1046] Odata Edm lib issue with vocabulary


    This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.

  • ODataLib 7.4.2

    Changes in ODataLib 7.4.2 Release

    Notes

    ODataLib 7.4.2 includes the following items: support for “Authorization” vocabularies to align with the Open API specification, enabling support for containment paths in navigation property bindings by addressing a bug, and various other fixes.

    Features

    [#1070] Add the Authorization vocabularies annotation into core edm model

    [#1109] Fix support for containment paths in nav prop bindings

    [#1112] Bug fix: Throw exception for an invalid enum value

    Fixed Bugs

    [Issue #645] Enable updating top-level properties to null.

    [Issue #1045] ODataUriExtensions.BuildUri ignores $apply

    [Issue #1084] Updating package ID to match the existing VSIX ID in the marketplace

    [Issue #1085] ExpressionLexer fix for parameter alias token in dotted expression

    [Issue #1092] Address StyleCop warnings


    This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.

  • ODataLib 7.4.3

    Changes in ODataLib 7.4.3 Release

    Notes

    ODataLib 7.4.3 fixes a minor bug introduced in 7.4.2 in which the path for a contained entity set was computed incorrectly.

    Fixed Bugs

    [Issue #1121] Incorrect path calculated for contained entity sets. [Issue #1086] Enable writing type annotations for collections in full metadata. —

    This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.

  • ODataLib 7.4.4

    Changes in ODataLib 7.4.4 Release

    Notes

    ODataLib 7.4.4 fixes a potential concurrency issue with an internal dictionary used for tracking navigation property mappings and adds code to make sure that navigation property bindings are never written for containment navigation properties.

    Fixed Bugs

    [Issue #137] Possible contention issues in navigationPropertyMappings dictionary. [Issue #138] Containment navigation properties shouldn’t define NavigationPropertyBindings to non-containment sets


    This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.

  • ODataLib 7.5.0

    Changes in ODataLib 7.5.0 Release

    Notes

    ODataLib 7.5.0 includes the following items: IN operator, Entity set aggregation, various bug fixes, and performance improvement.

    Features

    [#757] Entity set aggregations

    [#1165] [Feature] IN operator

    Fixed Bugs

    [#513] $select with complexCol/prop should be supported

    [#985] VS 2017 15.4.2 Broken client

    [#1043] Thread safety of context item collections

    [#1044] Code generation from T4 template - IgnoreUnexpectedElementsAndAttributes doesn’t seem to work

    [#1087] Batch is broken by load balancer (proxy)

    [#1148] OData Code Generator does not install on VS2015

    [#1123] Include dots in parseIdentifier for annotation parsing

    [#1130] Add StringComparison in String.Equal() method call

    [#1142] fix write document metadata properties for apply ComputeTransformationNode

    [#1143] fix build uri from groupby navigation property with many child structural properties

    [#1145] Memory Leak in Library?

    [#1151] Port client-side DateTime support fix to 7.x

    [#1153] UriParser should throw exception for mismatched keys count when UnqualifiedODataUriResolver is configured

    [#1155] FunctionCallBinder issue when case-insensitive is enabled

    [#1157] ODataMessageReader doesn’t honor the absence of ValidationKinds.ThrowIfTypeConflictsWithMetadata flag

    [#1159] remove the Document element from Edm lib

    [#1182] Fix a wrong if condition

    [#1191] Avoid String allocations when writing strings that require special character handling


    This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.

  • ODataLib 7.5.1

    Changes in ODataLib 7.5.1 Release

    Notes

    ODataLib 7.5.1 includes the following bug fixes:

    Fixed Bugs

    [#1181] Refactor the JSON string escape in JSON writer

    [#1216] Read parameter should not throw exception if missing optional parameter

    [#1220] Combined length of user strings exceeds allowed limit in OData V4 Code Generator

    [#1226] Cache the Edm full name for the Edm types

    [#1232] Fix lexer bug that breaks functions named ‘in’


    This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.

  • ODataLib 7.5.2

    Changes in ODataLib 7.5.2 Release

    Notes

    ODataLib 7.5.2 includes the following new features, bug fixes and improvements:

    Changes

    [#1272] Support $index query option

    [#1275] Support $apply in $expand

    [#855] Support OptionalParameter as an external annotation

    [#1262] OutOfLine Annotation for EnumMember throw exception when write to CSDL

    [#1276] OData nuget package description should change to reflect the latest status

    [#1287] Add Validation vocabulary annotation

    [#1110] Set-based operations

    [#1293] Update the Core vocabulary xml

    [#1286] Context url for expand

    [#1299] Enable write navigation property binding for containment target

    [#1302] Update the DerivedTypeConstraint term in Validation vocabulary

    [#1310] Add AllowedTerms/MinItems/MaxItems to Validation vocabulary

    [#1308] Add LocalDateTime type definition into Core vocabulary

    [#1312] Edm model validation for structural property type has wrong rule for property with type-definition type

    [#1314] Update DerivedTypeConstraint appliesTo

    [#1073] Better story for creating complex spatial types

    [#1296] Remove Contentions from Microsoft.OData.Edm

    [#1307] Add annotation term for Org.OData.Community.UrlEscape.V1.UrlEscapeFunction


    This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.

  • ODataLib 7.5.3

    Changes in ODataLib 7.5.3 Release

    Notes

    ODataLib 7.5.3 includes the following new features, bug fixes and improvements:

    Features

    [#1295] Enable derived type validation in Uri parsing

    [#1341] Support read/write Edm.PrimitiveType, Edm.ComplexType, Edm.EntityType

    [#1344] Enable expand transformation in $apply

    [#1346] Introduce new ODataResourceValue class and support read/write ODataResourceValue

    [#1358] Enable derived type validation in writing

    Fixed Bugs

    [#1323] Fix the escaped single-quote and Guid liternals for IN operator

    [#1359] Fix SelectExpandNode for navigation properties on derived/complex types

    Improvements

    [#1349] Remove locks from Uri parser

    [#1357] Use TryParse API instead of Parse for date to avoiding throwing exceptions


    This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.

  • ODataLib 7.5.4

    Changes in ODataLib 7.5.4 Release

    Notes

    ODataLib 7.5.4 includes the following new features, bug fixes and improvements:

    Features

    [#1376] Enable reading validation for derived type constraint annotation.

    [#1285] Support customizing the built-in vocabulary models.

    [#1404] Support reading/writing delta request payload.

    Fixed Bugs

    [#1368] Make aliases created in compute() transformation visible for following transforms/query options.

    [#1373] IN operator not working with null value on nullable properties.

    [#1385] & [#1164] Support parantheses and brackets in a CollectionConstantNode for IN operator.

    [#1390] ODataUriParser can’t parse for function with all omitted optional parameters.

    [#1391] Fix build uri problem with filter by Enum.

    Improvements

    [#1024] Improve the JSON reader buffer.


    This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.

  • ODataLib 7.6.beta

    Changes in ODataLib 7.6.beta Release

    Notes

    ODataLib 7.6.beta includes the following new features, bug fixes and improvements:

    Features

    [#1204] Support Large object stream (reader & writer).

    [#1400] Enable Uri parser to parse escape function.

    [#1414] Add the IEdmOperationReturn interface and enable annotation on return type of operation.

    [#1422] & [#1428] Update capabilities, validation & authorization vocabularies.

    [#1426] Properties defined in $compute and $apply could be used in a following query options ($select, $compute, $filter or $orderby).

    Fixed Bugs

    [#1260] Throw error when null value passed for collection of non-nullable complex type.

    [#1409] Ensure that we could use aliases created in compute() in groupby.

    [#1415] Build filter with “any” and “or” fails on keeping operations priority.

    Improvements

    [#1418] Use buffer when writing binary or byte array.

    [#1420] Use array pool in JSON writer.


    This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.

  • ODataLib 7.6.0

    Changes in ODataLib 7.6.0 Release

    Notes

    ODataLib 7.6.0 includes the following new features, bug fixes and improvements on top of O:

    Features

    [#1440] Add the Example Term into Core vocabulary.

    [#1454] Support versioned preferences for reading/writing OData prefix.

    [#1459] Write nested entity reference link(s) in request/response.

    [#1464] Enable to write the nextlink for the collection of entity reference links.

    [#1476] Support reading/writing Edmx with Version=4.01.

    Fixed Bugs

    [#1318] Unescaped colons in relative Uri cause Invalid URI exception.

    [#1451] FindType doesn’t work with alias-qualified names.

    [#1455] Remove bogus validation error for EntitySetPath.

    [#1463] Validate the resource type and resource set type in the same inheritance tree.

    [#1465] Support out of line annotations can’t target enum member.

    [#1467] Support enum parameters for Uri function.

    [#1469] Add a validation rule about target of the annotation should be allowed in the AppliesTo of the term.

    Improvements

    [#1448] Refactor & Improve the ODataMediaTypeResolver.

    [#1458] Align resource template and generated code.

    [#1473] Reduce ToList calls in operation overload resolver.

    [#1474] Fix tests disabled for large object streaming pull request.


    This release delivers OData core libraries including ODataLib, EdmLib, Spatial and Client.

5. ODATA FEATURES

  • Parsing uri path template

    From ODataLib 6.11.0, OData uri parser can parse uri path template. A path template is any identifier string enclosed with curly braces. For example:

    {dynamicProperty}

    Uri templates

    There are three kinds of template:

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

    Be caution:

    1. In UriParser instance, please set EnableUriTemplateParsing = true.
    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 EdmEnumMemberValue(1));
    colorType.AddMember("Blue", new EdmEnumMemberValue(2));
    
    EdmEnumMember enumMember = new EdmEnumMember(colorType, "Red", new EdmEnumMemberValue(3));
    colorType.AddMember(enumMember);
    
    EdmTerm stringTerm = new EdmTerm("DefaultNamespace", "StringTerm", EdmCoreModel.Instance.GetString(true));
    model.AddElement(stringTerm);
    
    var annotation = new EdmVocabularyAnnotation(
        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>
    
  • Add additional prefer header

    odata.track-changes, odata.maxpagesize, odata.ContinueOnError are supported to add in prefer header since ODataLib 6.11.0.

    Create request message with prefer header

    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()))
    }; 
    
    IEdmTerm term = model.FindTerm("Org.OData.Capabilities.V1.FilterRestrictions");
    if (term != null)  
    {  
      IEdmRecordExpression record = new EdmRecordExpression(properties);  
      EdmVocabularyAnnotation annotation = new EdmVocabularyAnnotation(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":[
        ...
      ]
    }
  • 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.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)
    }

    And in ODataLib 7.0, a custom converter is registered through DI.

    Sample

    Here we are trying to override the default converter to support the “R” format of date and time.

    1. Define DataTimeOffset converter

    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. Register new converter to DI container

    ContainerBuilderHelper.BuildContainer(
    	    builder => builder.AddService<ODataPayloadValueConverter, DateTimeOffsetCustomFormatPrimitivePayloadValueConverter>(ServiceLifetime.Singleton))

    Please refer here about DI details.

    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 now support serializing additional properties which are not advertised in metadata since ODataLib 6.13.0. To achieve this, users just need to turn off the ThrowOnUndeclaredPropertyForNonOpenType validation setting when constructing ODataMessageWriterSettings.

    Here is a full example which is trying to write an extra property Prop1 in the entity. The implementation of InMemoryMessage used in this sample can be found here.

    //Construct the model
    EdmModel model = new EdmModel();
    var entityType = new EdmEntityType("Namespace", "EntityType", null, false, true, false);
    entityType.AddKeys(entityType.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32, false));
    entityType.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(isNullable: true), null);
    
    model.AddElement(entityType);
    
    var container = new EdmEntityContainer("Namespace", "Container");
    var entitySet = container.AddEntitySet("EntitySet", entityType);
    
    // Create ODataMessageWriterSettings
    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
    {
        ODataUri = odataUri
    };
    settings.Validations &= ~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType;
    
    // Write the payload with extra property "Prop1"
    var entity = new ODataResource
    {
        Properties = new[]
        {
            new ODataProperty { Name = "ID", Value = 102 },
            new ODataProperty { Name = "Name", Value = "Bob" },
            new ODataProperty { Name = "Prop1", Value = "Var1" }
        }
    };
    
    using (var messageWriter = new ODataMessageWriter(message, settings, model))
    {
        ODataWriter writer = messageWriter.CreateODataResourceWriter(entitySet, entityType);
        writer.WriteStart(entity);
        writer.WriteEnd();
    
        outputStream.Seek(0, SeekOrigin.Begin);
        var output = new StreamReader(outputStream).ReadToEnd();
        Console.WriteLine(output);
        Console.ReadLine();
    }

    Prop1 will appear in the payload:

    {
        "@odata.context":"http://example.org/odata.svc/$metadata#EntitySet/$entity",
        "ID":102,
        "Name":"Bob",
        "Prop1":"Var1"
    }
  • Expanded Navigation Property Support in Delta Response

    From ODataLib 6.15.0, we introduced the support for reading and writing expanded navigation properties (either collection or single) in delta responses. This feature is not covered by the current OData spec yet but the official protocol support is already in progress. As far as the current design, expanded navigation properties can ONLY be written within any $entity part of a delta response. Every time an expanded navigation property is written, the full expanded resource set or resource should be written instead of just the delta changes because in this way it’s easier to manage the association among resources consistently. Inside the expanded resource set or resource, there are ONLY normal resource sets or resources. Multiple expanded navigation properties in a single $entity part is supported. Containment is also supported.

    Basically the new APIs introduced are highly consistent with the existing ones for reading and writing normal delta responses so there should not be much trouble implementing this feature in OData services. This section shows how to use the new APIs.

    Write expanded navigation property in delta response

    There are only four new APIs introduced for writing.

    public abstract class Microsoft.OData.ODataDeltaWriter
    {
        ...
        public abstract void WriteStart (Microsoft.OData.ODataResourceSet expandedResourceSet)
        public abstract void WriteStart (Microsoft.OData.ODataNestedResourceInfo nestedResourceInfo) 
        public abstract System.Threading.Tasks.Task WriteStartAsync (Microsoft.OData.ODataResourceSet expandedResourceSet)
        public abstract System.Threading.Tasks.Task WriteStartAsync (Microsoft.OData.ODataNestedResourceInfo nestedResourceInfo)
        ...
    }

    The following sample shows how to write an expanded resource set (collection of resources) in a delta response. Please note that regardless of whether or not the nested resource info will be eventually written to the payload, WriteStart(nestedResourceInfo) MUST be called before actually calling WriteStart(expandedResourceSet) to write an expanded resource set. So is for a single expanded resource.

    ODataDeltaWriter writer = messageWriter.CreateODataDeltaWriter(customersEntitySet, customerType);
    writer.WriteStart(deltaResourceSet);               // delta resource set
    writer.WriteStart(customerResource);           // delta resource
    writer.WriteStart(ordersNestedResourceInfo);    // nested resource info
    writer.WriteStart(ordersResourceSet);              // normal expanded resource set
    writer.WriteStart(orderResource);              // normal resource
    writer.WriteEnd();  // orderResource
    writer.WriteEnd(); // ordersResourceSet
    writer.WriteEnd(); // ordersNestedResourceInfo
    writer.WriteEnd(); // customerResource
    writer.WriteEnd(); // deltaResourceSet
    writer.Flush();
    
    string payloadLooksLike =
        "{" +
            "\"@odata.context\":\"http://host/service/$metadata#Customers/$delta\"," +
            "\"value\":" +
            "[" + // deltaResourceSet
                "{" + // customerResource
                    "\"@odata.id\":\"http://host/service/Customers('BOTTM')\"," +
                    "\"ContactName\":\"Susan Halvenstern\"," +
                    "\"Orders\":" + // ordersNestedResourceInfo
                    "[" + // ordersResourceSet
                        "{" + // orderResource
                            "\"@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(deltaResourceSet);                           // delta resource set
    writer.WriteStart(customerResource);                       // delta resource
    writer.WriteStart(productBeingViewedNestedResourceInfo);    // nested resource info
    writer.WriteStart(productResource);                        // normal expanded resource
    writer.WriteStart(detailsNestedResourceInfok);               // nested resource info
    writer.WriteStart(detailsResourceSet);                         // nested expanded resource set
    writer.WriteStart(productDetailResource);                  // normal resource
    writer.WriteEnd(); // productDetailResource
    writer.WriteEnd(); // detailsResourceSet
    writer.WriteEnd(); // detailsNestedResourceInfo
    writer.WriteEnd(); // productResource
    writer.WriteEnd(); // productBeingViewedNestedResourceInfo
    writer.WriteEnd(); // customerResource
    writer.WriteEnd(); // deltaResourceSet
    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(resource), ODataJsonLightDeltaWriter keeps track of an internal state machine thus can correctly differentiate between writing a delta resource and writing a normal resource. Actually during writing the expanded navigation properties, all calls to WriteStart(resource), WriteStart(resourceSet) and WriteStart(nestedResourceInfo) 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 resource but will skip writing any structural property or instance annotation until it begins to write a nested resource info (means we are going to write an expanded navigation property).

    Read expanded navigation property in delta response

    In the reader part, new APIs include a new state enum and a sub state property. All the other remains the same.

    public enum Microsoft.OData.ODataDeltaReaderState
    {
        ...
        NestedResource = 10
        ...
    }
    
    public abstract class Microsoft.OData.ODataDeltaReader
    {
        ...
        Microsoft.OData.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.NestedResource. 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 resource set and resource in a delta payload.

    ODataDeltaReader reader = messageReader.CreateODataDeltaReader(customersEntitySet, customerType);
    while (reader.Read())
    {
        switch (reader.State)
        {
            case ODataDeltaReaderState.DeltaResourceSetStart:
                // Start delta resource set
                ...
            case ODataDeltaReaderState.DeltaResourceSetEnd:
                // End delta resource set
                ...
            case ODataDeltaReaderState.DeltaResourceStart:
                // Start $entity (may be followed by an NestedResource)
                ...
            case ODataDeltaReaderState.DeltaResourceEnd:
                // End $entity
                ...
            case ODataDeltaReaderState.NestedResource:
                switch (reader.SubState)
                {
                    case ODataReaderState.ResourceSetStart:
                        var resourceSet = reader.Item as ODataResourceSet;
                        ...
                    case ODataReaderState.ResourceStart:
                        var resource = reader.Item as ODataResource;
                        ...
                    case ODataReaderState.NestedResourceInfoStart:
                        var nestedResourceInfo = reader.Item as ODataNestedResourceInfo;
                        ...
                    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 ODataResource being read out has no metadata builder with it. Actually the expanded payloads are treated in the same way as the nested payloads when reading parameters. This is currently a limitation which means the service CANNOT get the metadata links from a normal entity in a delta response. The internal reader also skips the next links after an expanded resource set because the $entity part of a delta payload is non-pageable. When the internal reader is consuming the expanded payload, the delta reader remains at the NestedResource 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:

  • Navigation property under complex type

    Since OData V7.0, it supports to add navigation property under complex type. Basically navigation under complex are same with navigation under entity for usage, the only differences are: 1. Navigation under complex can have multiple bindings with different path. 2. Complex type does not have id, so the navigation link and association link of navigation under complex need contain the entity id which the complex belongs to.

    The page will include the usage of navigation under complex in EDM, Uri parser, and serializer and deserializer.

    1. EDM

    Define navigation property to complex type

    EdmModel model = new EdmModel();
    
    EdmEntityType city = new EdmEntityType("Sample", "City");
    EdmStructuralProperty cityId = city.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(false));
    city.AddKeys(cityId);
    
    EdmComplexType complex = new EdmComplexType("Sample", "Address");
    complex.AddStructuralProperty("Road", EdmCoreModel.Instance.GetString(false));
    EdmNavigationProperty navUnderComplex = complex.AddUnidirectionalNavigation(
        new EdmNavigationPropertyInfo()
        {
            Name = "City",
            Target = city,
            TargetMultiplicity = EdmMultiplicity.One,
        });
    
    model.AddElement(city);
    model.AddElement(complex);

    Please note that only unidirectional navigation is supported, since navigation property must be entity type, so bidirectional navigation property does not make sense to navigation under complex type.

    Add navigation property binding for navigation property under complex

    When entity type has complex as property, its corresponding entity set can bind the navigation property under complex to a specified entity set. Since multiple properties can be with same complex type, the navigation property under complex can have multiple bindings with different path.

    A valid binding path for navigation under complex is: [ qualifiedEntityTypeName "/" ] *( ( complexProperty / complexColProperty ) "/" [ qualifiedComplexTypeName "/" ] ) navigationProperty

    For example:

    EdmEntityType person = new EdmEntityType("Sample", "Person");
    EdmStructuralProperty entityId = person.AddStructuralProperty("UserName", EdmCoreModel.Instance.GetString(false));
    person.AddKeys(entityId);
    
    person.AddStructuralProperty("Address", new EdmComplexTypeReference(complex, false));
    person.AddStructuralProperty("Addresses", new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(complex, false))));
    
    model.AddElement(person);
    
    var entityContainer = new EdmEntityContainer("Sample", "Container");
    model.AddElement(entityContainer);
    EdmEntitySet people = new EdmEntitySet(entityContainer, "People", person);
    EdmEntitySet cities1 = new EdmEntitySet(entityContainer, "Cities1", city);
    EdmEntitySet cities2 = new EdmEntitySet(entityContainer, "Cities2", city);
    people.AddNavigationTarget(navUnderComplex, cities1, new EdmPathExpression("Address/City"));
    people.AddNavigationTarget(navUnderComplex, cities2, new EdmPathExpression("Addresses/City"));
    
    entityContainer.AddElement(people);
    entityContainer.AddElement(cities1);
    entityContainer.AddElement(cities2);

    The navigation property navUnderComplex is binded to cities1 and cities2 with path "Address/City" and "Addresses/City" respectively.

    Then the csdl of the model would be like:

    <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="Sample">
        <EntityType Name="City">
            <Key>
                <PropertyRef Name="Name"/>
            </Key>
            <Property Name="Name" Nullable="false" Type="Edm.String"/>
        </EntityType>
        <ComplexType Name="Address">
            <Property Name="Road" Type="Edm.String" Nullable="false" />
            <NavigationProperty Name="City" Nullable="false" Type="Sample.City"/>
        </ComplexType>
        <EntityType Name="Person">
            <Key>
                <PropertyRef Name="UserName"/>
            </Key>
            <Property Name="UserName" Nullable="false" Type="Edm.String"/>
            <Property Name="Address" Nullable="false" Type="Sample.Address"/>
            <Property Name="Addresses" Nullable="false" Type="Collection(Sample.Address)"/>
        </EntityType>
        <EntityContainer Name="Container">
            <EntitySet Name="People" EntityType="Sample.Person">
                <NavigationPropertyBinding Target="Cities1" Path="Address/City"/>
                <NavigationPropertyBinding Target="Cities2" Path="Addresses/City"/>
            </EntitySet>
            <EntitySet Name="Cities1" EntityType="Sample.City"/>
            <EntitySet Name="Cities2" EntityType="Sample.City"/>
        </EntityContainer>
    </Schema>
    

    The binding path may need include type cast. For example, if there is a navigation property City2 defined in a complex type UsAddress which is derived from Address. If add a binding to City2, it should be like this: people.AddNavigationTarget(navUnderDerivedComplex, cities1, new EdmPathExpression("Address/Sample.UsAddress/City2")); Here we do not include type cast sample to keep the scenario simple.

    Find navigation target for navigation property under complex

    Since a navigation property can be binded to different paths, the exact binding path must be specified for finding the navigation target. For example:

    IEdmNavigationSource navigationTarget = people.FindNavigationTarget(navUnderComplex, new EdmPathExpression("Address/City"));

    Cities1 will be returned for this case.

    2. Uri

    Query

    Here lists some sample valid query Uris to access navigation property under complex:

    Path

    http://host/People('abc')/Address/City

    Accessing navigation property under collection of complex is not valid, since item in complex collection does not have a canonical Url. That is to say, http://host/People('abc')/Addresses/City is not valid, City under Addresses can only be accessed through $expand.

    Query option

    Different with path, navigation under collection of complex can be accessed directly in expressions of $select and $expand, which means Addresses/City is supported. Refer ABNF for more details.

    $select:  
    http://host/People('abc')/Address?$select=City
    http://host/People?$select=Address/City
    http://host/People?$select=Addresses/City
    
    $expand: 
    http://host/People('abc')/Address?$expand=City
    http://host/People?$expand=Address/City
    http://host/People?$expand=Addresses/City
    
    $filter:
    http://host/People?$filter=Address/City/Name eq 'Shanghai'
    http://host/People('abc')/Addresses?$filter=City/Name eq 'Shanghai'
    http://host/People?$filter=Addresses/any(a:a/City/Name eq 'Shanghai')
    
    $orderby:
    http://host/People?$order=Address/City/Name
    

    Uri parser

    There is nothing special if using ODataUriParser. For ODataQueryOptionParser, if we need resolve the navigation property under complex to its navigation target in the query option, navigation source that the complex belongs to and the binding path are both needed. If the navigation source or part of binding path is in the path, we need it passed to the constructor of ODataQueryOptionParser. So there are 2 overloaded constructor added to accept ODataPath as parameter.

    public ODataQueryOptionParser(IEdmModel model, ODataPath odataPath, IDictionary<string, string> queryOptions)
    public ODataQueryOptionParser(IEdmModel model, ODataPath odataPath, IDictionary<string, string> queryOptions, IServiceProvider container)

    Note: Parameter IServiceProvider is related to Dependency Injection.

    Actually we do not recommend to use ODataQueryOptionParser in this case, ODataUriParser would be more convenient. Here we still give an example just in case:

    // http://host/People('abc')/Address?$expand=City
    ODataUriParser uriParser = new ODataUriParser(Model, ServiceRoot, new Uri("http://host/People('abc')/Address"));
    ODataPath odataPath = uriParser.ParsePath();
    ODataQueryOptionParser optionParser = new ODataQueryOptionParser(Model, odataPath, new Dictionary<string, string> { { "$expand", "City" } });
    SelectExpandClause clause = optionParser.ParseSelectAndExpand();
    
    // This can achieve same result.
    uriParser = new ODataUriParser(Model, ServiceRoot, new Uri("http://host/People('abc')/Address?$expand=City"));
    clause = uriParser.ParseSelectAndExpand();

    3. Serializer (Writer)

    Basically, the writing process is same with writing navigation under entity. Let’s say we are writing an response of query http://host/People('abc')?$expand=Address/City.

    Sample code:

    var uriParser = new ODataUriParser(Model, ServiceRoot, new Uri("http://host/People('abc')?$expand=Address/City"));
    var odataUri = uriParser.ParseUri();
    settings.ODataUri = odataUri;// Specify the odataUri to ODataMessageWriterSettings, which will be reflected in the context url.
    
    ODataResource res = new ODataResource() { Properties = new[] { new ODataProperty { Name = "UserName", Value = "abc" } } };
    ODataNestedResourceInfo nestedComplexInfo = new ODataNestedResourceInfo() { Name = "Address" };
    ODataResource nestedComplex = new ODataResource() { Properties = new[] { new ODataProperty { Name = "Road", Value = "def" } } };
    ODataNestedResourceInfo nestedResInfo = new ODataNestedResourceInfo() { Name = "City", IsCollection = false };
    ODataResource nestednav = new ODataResource() { Properties = new[] { new ODataProperty { Name = "Name", Value = "Shanghai" } } };
    
    // Ignore code to CreateODataResourceWriter.
    
    writer.WriteStart(res);
    writer.WriteStart(nestedComplexInfo);
    writer.WriteStart(nestedComplex);
    writer.WriteStart(nestedResInfo);
    writer.WriteStart(nestednav);
    writer.WriteEnd();   // End of City
    writer.WriteEnd();   // End of City nested info
    writer.WriteEnd();// End of complex
    writer.WriteEnd();// End of complex info
    writer.WriteEnd();// End of entity

    Payload:

    {
    "@odata.context": "http://host/$metadata#People/$entity",
    "UserName":"abc",
    "Address":
    {
       "Road":"def",
       "City":
       {
           "Name":"Shanghai"
       }
    }
    }
    

    4. Deserializer (Reader)

    Reading process is same with reading an navigation property under entity. For navigation property City under Address, it will be read as an ODataNestedResourceInfo which has navigation url http://host/People('abc')/Complex/City and an ODataResource which has Id http://host/Cities1('Shanghai').

  • Merge complex type and entity type

    From ODataLib 7.0, we merged the public APIs for serializing/deserializing complex values and entities. We did this because complex type and entity type are becoming more and more similar in the protocol, but we continue to pay overhead to make things work for both of them. Since the only really fundamental differences between complex type and entity type at this point are the presence of a key and media link entries, we thought it was best to merge them and just deal with the differences.

    We followed the existing implementation of serializing/deserializing entity instances for both entity instances and complex instances, and the implementation of serializing/deserializing entity collections for both entity collections and complex collections. This page will provide some simple sample code about how to write these kinds of payload.

    Public APIs used to store complex/entity instances

    ODataResource class is used to store information of an entity instance or a complex instance.

    ODataResourceSet class is used for both a collection of entity or a collection of complex.

    ODataNestedResourceInfo class is used for both navigation property and complex property. For complex property, this class will be used to store the name of the complex property and a Boolean to indicate whether this property is a single instance or a collection.

    For other Public APIs, you can refer to the Breaking changes about Merge Entity and Complex.

    Model

    Suppose we have a model, in following sections, we will explain how to write/read a complex property or a collection complex property.

    <?xml version="1.0" encoding="utf-8"?>
    <edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
      <edmx:DataServices>
        <Schema Namespace="SampleService" xmlns="http://docs.oasis-open.org/odata/ns/edm">
          <ComplexType Name="Location" OpenType="true">
            <Property Name="Address" Type="Edm.String" Nullable="false" />
            <Property Name="City" Type="Edm.String" Nullable="false" />
          </ComplexType>
          <EntityType Name="Person" OpenType="true">
            <Key>
              <PropertyRef Name="UserName" />
            </Key>
            <Property Name="UserName" Type="Edm.String" Nullable="false" />
            <Property Name="HomeAddress" Type="SampleService.Location" />
            <Property Name="OtherAddresses" Type="Collection(SampleService.Location)" />
            </Property>
          </EntityType>
          <EntityContainer Name="DefaultContainer">
            <EntitySet Name="People" EntityType="SampleService.Person" />
          </EntityContainer>
        </Schema>
      </edmx:DataServices>
    </edmx:Edmx>
    

    Write a top level complex instance

    In order to write the complex property payload, we need to create an ODataWriter first. ODataLib provides ODataMessageWriter.CreateODataResourceWriter to create the writer.

        IODataResponseMessage message;
        IEdmStructuredType complexType;
        // Initialize the message and the complexType;
        // message = ...;
        // complexType = ...;
        var settings = new ODataMessageWriterSettings { Version = ODataVersion.V4 };
        settings.ODataUri = http://localhost/odata/;
        ODataMessageWriter messageWriter = new ODataMessageWriter(message, settings, model);
        var writer = messageWriter.CreateODataResourceWriter(null, complexType);

    Then, we can write a complex property just like writing an entity by using WriteStart and WriteEnd.

        ODataResource complexResource = new ODataResource()
        {
            Properties = new ODataProperty[]
            {
                new ODataProperty { Name = "Address", Value = "Zixing Road" },
                new ODataProperty { Name = "City", Value = "Shanghai" }
            }
        };
    
        writer.WriteStart(complexResource);
        writer.WriteEnd();

    If we want to write a complex collection property, we can use CreateODataResourceSetWriter and write the complex collection property.

        writer = messageWriter.CreateODataResourceSetWriter(null, complexType /* item type */);
        ODataResourceSet complexCollection = new ODataResourceSet()
        ODataResource complexResource = new ODataResource()
        {
            Properties = new ODataProperty[]
            {
                new ODataProperty { Name = "Address", Value = "Zixing Road" },
                new ODataProperty { Name = "City", Value = "Shanghai" }
            }
        };
    
        writer.WriteStart(complexCollection); // write the resource set.
        writer.WriteStart(complexResource); // write each resource.
        writer.WriteEnd(); // end the resource.
        writer.WriteEnd(); // end the resource set.

    Write a complex property in an entity instance

    To write an entity with a complex property, we can create the ODataWriter for the entity by calling CreateODataResourceWriter, and then write the entity. we write the complex property just as writing a navigation property.

    • Write the property name of the complex property by WriteStart(ODataNestedResourceInfo nestedResourceInfo)
    • Write the complex instance by WriteStart(ODataResource resource).
    • WriteEnd() for each part.

    Sample:

        // Init the entity instance.
        ODataResource entityResource = new ODataResource()
        {
    	Properties = new ODataProperty[]
            {...}
        }
        
        // Create the ODataNestedResourceInfo for the HomeAddress property.
        ODataNestedResourceInfo homeAddressInfo = new ODataNestedResourceInfo() { Name = "HomeAddress", IsCollection = false };
        ODataResource complexResource = new ODataResource()
        {
            Properties = new ODataProperty[]
            {
                new ODataProperty { Name = "Address", Value = "Zixing Road" },
                new ODataProperty { Name = "City", Value = "Shanghai" }
            }
        };
    
        writer.WriteStart(homeAddressInfo); // write the nested resource info. 
        writer.WriteStart(complexResource); // write then resource.
        writer.WriteEnd(); // end the resource.
        writer.WriteEnd(); // end the nested resource info

    To write a complex collection property in an entity, we need to set ODataNestedResourceInfo.IsCollection is set to true, and then write the resource set.

        // Init the entity instance.
        ODataResource entityResource = new ODataResource()
        {
    	Properties = new ODataProperty[]
            {...}
        }
    
        // Create the ODataNestedResourceInfo for the OtherAddresses property.
        ODataNestedResourceInfo otherAddressesInfo = new ODataNestedResourceInfo() { Name = "OtherAddresses", IsCollection = true };
        ODataResourceSet complexCollection = new ODataResourceSet()
        ODataResource complexResource = new ODataResource()
        {
            Properties = new ODataProperty[]
            {
                new ODataProperty { Name = "Address", Value = "Zixing Road" },
                new ODataProperty { Name = "City", Value = "Shanghai" }
            }
        };
    
        writer.WriteStart(entityResource); // write the entity instance.
        writer.WriteStart(otherAddressesInfo); // write the nested resource info.
        writer.WriteStart(complexCollection); // write the resource set.
        writer.WriteStart(complexResource); // write each resource.
        writer.WriteEnd(); // end the resource.
        writer.WriteEnd(); // end the resource set.
        writer.WriteEnd(); // end the nested resource info
        writer.WriteEnd(); // end the entity instance.

    Developers can follow the same way to write a nested complex property in a complex instance.

    Write complex or entity instances in Uri parameters

    To write an entity (or a complex) instance or a collection of entity (or a collection of complex) in Uri parameters, ODataLib provides ODataMessageWriter.CreateODataUriParameterResourceWriter and ODataMessageWriter.CreateODataUriParameterResourceSetWriter to create the ODataWriter. Then, you can follow the same sample code in the above two sections to write the related parameters.

    Read a top level complex instance

    To read a complex (or an entity) instance, ODataLib provides ODataMessageReader.CreateODataResourceReader to create the ODataReader.

        IODataResponseMessage message;
        IEdmStructuredType complexType;
        // Initialize the message and the complexType;
        // message = ...;
        // complexType = ...;
        var settings = new ODataMessageReaderSettings { EnableMessageStreamDisposal = true };
        ODataMessageReader messageReader = new ODataMessageReader(message, settings, model);
        var reader = messageReader.CreateODataResourceReader(null, complexType);

    Then, developers can read the complex (or entity) instance, nested complex ( or complex collection) properties or navigation properties.

        while (reader.Read())
        {
             switch (reader.State)
             {
                 case ODataReaderState.ResourceEnd:
                     // reader.Item is a resource which reprensents the complex instance or its nested resources 
                     // 1. nested complex instance of a complex property
                     // 2. nested complex instances of a complex collection property
                     // 3. entity instances or a navigation property.
                     complex = reader.Item as ODataResource;
                     break;
                 case ODataReaderState.ResourceSetEnd:
                     // reader.Item is a resource set which represents a collection of complex or entity.
                     // 1. nested complex collection
                     // 2. navigated entity collection
                     complex = reader.Item as ODataResourceSet;
                     break;
                 case ODataReaderState.NestedResourceInfoEnd:
                     // reader.Item is a the nested resource info which represents a complex property, a complex collection property or a navigation property.
                     complex = reader.Item as ODataNestedResourceInfo;
                     break;
                 default:
                     break;
             }
        }

    if this reader is created for an entity type, the same code can be used to read an entity instance.

    Read a top level complex collection

    To read a complex collection ( or an entity collection) , ODataLib provides ODataMessageReader.CreateODataResourceSetReader to create the ODataReader.

        var reader = messageReader.CreateODataResourceSetReader(null, complexType);
        while (reader.Read())
        {
             switch (reader.State)
             {
                 case ODataReaderState.ResourceEnd:
                     // reader.Item is a resource which reprensents the complex instance or its nested resources
                     // 0. the top level complex instance.  
                     // 1. nested complex instance of a complex property.
                     // 2. nested complex instances of a complex collection property.
                     // 3. entity instances or a navigation property.
                     complex = reader.Item as ODataResource;
                     break;
                 case ODataReaderState.ResourceSetEnd:
                     // reader.Item is a resource set which represents a collection of complex or entity.
                     // 0. the top level complex collection.
                     // 2. netsed complex collection.
                     // 2. navigated entity collection.
                     complex = reader.Item as ODataResourceSet;
                     break;
                 case ODataReaderState.NestedResourceInfoEnd:
                     // reader.Item is a the nested resource info which represents a complex property, a complex collection property or a navigation property.
                     complex = reader.Item as ODataNestedResourceInfo;
                     break;
                 default:
                     break;
             }
        }

    if this reader is created for an entity collection, the same code can be used to read an entity collection.

    Read complex or entity instances in Uri parameters

    To read an entity (or a complex) instance or a collection of entity (or a collection of complex) in Uri parameters, ODataLib provides ODataMessageReader.CreateODataUriParameterResourceReader and ODataMessageReader.CreateODataUriParameterResourceSetReader to create the ODataReader. Then, you can follow the same sample code in the above two sections to read the related parameters.

  • OData Uri Path Parser Extensibility

    In order to support more comprehensive OData Uri path, from ODataLib 7.0, we support Uri path parser customization in two parts:

    • Allow developers to customize how to separate a Uri into segments in string.
    • Allow developers to customize how to bind those raw string segments unrecognized by ODataUriParser to the model and create ODataPathSegments.

    For example, we have a Uri http://localhost/odata/drives(‘C’)/root:/OData/Docs/Features/Uri%20Parser%20Path%20Extensibility.doc:/size which is used to get the size of a local file “C:\OData\Docs\Features\Uri Parser Path Extensible.doc”. But ODataUriParser.ParsePath() doesn’t know how to bind this path to the edm model. So we want to provide a way to let developers define how to parse it.

    Following sections will provide some sample codes to support this feature.

    Model

    Given a model as following:

    <?xml version="1.0" encoding="utf-8"?>
    <edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
      <edmx:DataServices>
        <Schema Namespace="SampleService" xmlns="http://docs.oasis-open.org/odata/ns/edm">
          <EntityType Name="drive">
            <Key>
              <PropertyRef Name="id" />
            </Key>
            <Property Name="id" Type="Edm.String" Nullable="false" />
            <NavigationProperty Name="items" Type="Collection(SampleService.item)" ContainsTarget="true" />
          </EntityType>
          <EntityType Name="item" OpenType="true">
            <Key>
              <PropertyRef Name="id" />
            </Key>
            <Property Name="id" Type="Edm.String" Nullable="false" />
            <Property Name="size" Type="Edm.Int64" />
          </EntityType>
          <EntityContainer Name="SampleService">
            <EntitySet Name="drives" EntityType="SampleService.drive" />
          </EntityContainer>
        </Schema>
      </edmx:DataServices>
    </edmx:Edmx>
    

    Separate a Uri into Segments

    UriPathParser provides a public virtual API ParsePathIntoSegments(Uri fullUri, Uri serviceBaseUri) to customize how to separate a Uri into segments in raw string.

    Developers can define their own UriPathParser, register this class by DI (Please refer to Dependency Injection support) and override the ParsePathIntoSegments. Then, ODataLib will use this API to separate the Uri into several segments.

    public class UriPathParser
    {
        public virtual ICollection<string> ParsePathIntoSegments(Uri fullUri, Uri serviceBaseUri)
    }
    

    Customized UriPathParser

    public class CustomizedUriPathParser : UriPathParser
    {
        public UriPathParserType UriPathParserType { get; set; }
        public CustomizedUriPathParser(ODataUriParserSettings settings)
            : base(settings)
        { }
    
        public override ICollection<string> ParsePathIntoSegments(Uri fullUri, Uri serviceBaseUri)
        {
            Uri uri = fullUri;
            int numberOfSegmentsToSkip = 0;
    
            numberOfSegmentsToSkip = serviceBaseUri.AbsolutePath.Split('/').Length - 1;
            string[] uriSegments = uri.AbsolutePath.Split('/');
    
            List<string> segments = new List<string>();
            List<string> tmpSegments = new List<string>();
            bool end = true;
            for (int i = numberOfSegmentsToSkip; i < uriSegments.Length; i++)
            {
                string segment = uriSegments[i];
                if (!segment.StartsWith("root:") && segment.EndsWith(":"))
                {
                    tmpSegments.Add(segment);
                    segments.Add(Uri.UnescapeDataString(string.Join("/", tmpSegments)));
                    end = true;
                }
                else if (segment.StartsWith("root:") || !end)
                {
                    end = false;
                    tmpSegments.Add(segment);
                    continue;
                }
                else
                {
                    segments.Add(segment);
                }
            }
    
            return segments.ToArray();
        }
    }
    

    This class defines its own ParsePathIntoSegments to separate the Uri into segments. Developers can register this class by builder.AddService<UriPathParser, CustomizedUriPathParser>(ServiceLifetime.Scoped).

    ParsePathIntoSegments considers “root:/OData/Docs/Features/Uri%20Parser%20Path%20Extensibility.doc:” as one segment. It parsed the whole Uri into three segments in string.

    [0]: "drives('C')"
    [1]: "root:/OData/Docs/Features/Uri Parser Path Extensibility.doc:"
    [2]: "size"
    

    Bind Unknown Raw-string Segments to Model

    After the above step, developers now can define how to bind the “root:/OData/Docs/Features/Uri Parser Path Extensibility.doc:” which is unknown for ODataUriParser.

    For all unknown segments, ODataLib provides a DynamicPathSegment class to represent the meaning of them in model. DynamicPathSegment could be used for both open property segments or other dynamic path segments.

    ODataUriParser also provides a Property ParseDynamicPathSegmentFunc for developers to bind those unknown raw-string segments to model.

    public delegate ICollection<ODataPathSegment> ParseDynamicPathSegment(ODataPathSegment previous, string identifier, string parenthesisExpression);
    
    public ParseDynamicPathSegment ParseDynamicPathSegmentFunc
    {
        get { return this.configuration.ParseDynamicPathSegmentFunc; }
        set { this.configuration.ParseDynamicPathSegmentFunc = value; }
    }
    

    By default, if developers do not set the ParseDynamicPathSegmentFunc , ODataLib will consider the unknown segment as an open property. Or, ODataLib will use this function to bind the segment and return a collection of ODataPathSegment. Then, ODataLib will parse the following segment according to this binding result.

    1. Developers can set ParseDynamicPathSegmentFunc as following which parses the second segment into a DynamicPathSegment with IEdmEntityType “Item”. Then ODataUriParser will parse the last segment “size” into a PropertySegment.

       uriParser.ParseDynamicPathSegmentFunc = (previous, identifier, parenthesisExpression) =>
       {
           switch (identifier)
           {
               case "root:/OData/Docs/Features/Uri Parser Path Extensibility.doc:":
                   return new List<ODataPathSegment> { new DynamicPathSegment(identifier, itemType, true) };
               default:
                   throw new Exception("Not supported Type");
           }
       };
      

      Then, the ODataPath should be:

       [0]: EntitySetSegment : "drives"
       [1]: KeySegment : "C"
       [2]: DynamicPathSegment: "root:/OData/Docs/Features/Uri Parser Path Extensibility.doc:"
       [3]: PropertySegment: "size"
      
    2. Developers can also set ParseDynamicPathSegmentFunc as following which will translate the unknown string segment into a NavigationPropertySegment and KeySegment:

       uriParser.ParseDynamicPathSegmentFunc = (previous, identifier, parenthesisExpression) =>
       {
           switch (identifier)
           {
               case "root:/OData/Docs/Features/Uri Parser Path Extensibility.doc":
                   return new List<ODataPathSegment>
                   {
                       new NavigationPropertySegment(navProp, navSource);,
                       new KeySegment(new Dictionary<string, object>() { { "id", "01VL3Q7L36JOJUAPXGDNAZ4FVIGCTMLL46" } }, itemType, navSource);
                   };
               default:
                   throw new Exception("Not supported Type");
           }
       };
      

      Then, the ODataPath should be:

       [0]: EntitySetSegment : "drives"
       [1]: KeySegment : "C"
       [2]: NavigationPropertySegment: "items"
       [3]: KeySegment: "01VL3Q7L36JOJUAPXGDNAZ4FVIGCTMLL46"
       [4]: PropertySegment: "size"
      

    On server side, developers can implement their own behavior of CRUD according to the parse result.

  • Navigation property partner

    The library supports setting and retrieving partner information of navigation properties.

    The following APIs can be used to set partner information:

    public class EdmEntityType
    {
        ...
        public void SetNavigationPropertyPartner(
            EdmNavigationProperty navigationProperty,
            IEdmPathExpression navigationPropertyPath,
            EdmNavigationProperty partnerNavigationProperty,
            IEdmPathExpression partnerNavigationPropertyPath);
        public EdmNavigationProperty AddBidirectionalNavigation(
            EdmNavigationPropertyInfo propertyInfo,
            EdmNavigationPropertyInfo partnerInfo);
        ...
    }

    The former is general-purpose while the latter is a convenient shortcut for certain scenarios. Let’s look at the first method. To use this method, you must first have a top-level navigation property navigationProperty already defined on the entity type, and another possibly nested navigation property partnerNavigationProperty already defined on the target entity type. This method then sets the partner information of the two navigation properties. Since navigationProperty is a top-level navigation property defined on the entity type on which the method is invoked, navigationPropertyPath is simply the name of the navigation property. partnerNavigationProperty, on the other hand, could be defined on either an entity or complex type. partnerNavigationPropertyPath specifies the path to the partner navigation property on the target entity type. For example, if the partner navigation property is called InnerNav which is defined on a complex-typed property ComplexProp which is a top-level structural property defined on the target entity type, then partnerNavigationPropertyPath should be passed something like new EdmPathExpression("ComplexProp/InnerNav"). According to the spec, the partner information must not be set for navigation properties defined on a complex type. So, in this case, partner information will only be set on navigationProperty, but not partnerNavigationProperty. Let’s give another example, say the partner navigation property is called Nav which is a top-level navigation property defined on the target entity type. Then partnerNavigationPropertyPath should be passed something like new EdmPathExpression("Nav"). In this case, the partner information will be set for both navigationProperty and partnerNavigationProperty.

    The second method is a shortcut for the case when both navigation properties are top-level properties defined on the entity types. It doesn’t work for the case when one navigation property is defined on a complex type, and thus a nested property on the entity type. It doesn’t require you to define the navigation properties first; it will do this on your behalf. In effect, it will first add the two navigation properties on the entity types, and then set their partner information.

    The following APIs are provided to retrieve the partner information of a navigation property:

    public interface IEdmNavigationProperty
    {
        ...
        IEdmNavigationProperty Partner { get; }
        ...
    }
    
    public static class ExtensionMethods
    {
        ...
        public static IEdmPathExpression GetPartnerPath(
            this IEdmNavigationProperty navigationProperty);
        ...
    }

    The first is used to retrieve the partner navigation property itself, while the second is used to retrieve the path to the partner navigation property within the target entity type.

  • Override type annotation in serialization

    As you may know, all the OData items (ODataResource, ODataResourceSet, etc.) read from the payload are inherited from ODataAnnotatable. In ODataAnnotatable, there is a property called TypeAnnotation whose type is ODataTypeAnnotation. It is used to store the @odata.type annotation read from or written to the payload. The reasons why we don’t merge it into the instance annotations in ODataAnnotatable are that: 1) for performance improvement; 2) in ATOM format, we also have OData type annotation.

    During deserialization, the TypeAnnotation property will be set by the OData readers into each OData item read from the payload. During serialization, the TypeAnnotation property itself and its TypeName property together will control how OData type annotation will be written to the payload.

    The control policies are:

    • If the TypeAnnotation property itself is null, then we will rely on the underlying logic in ODataLib and Web API OData to determine what OData type annotation to be written to the payload.
    • If the TypeAnnotation property is not null but the TypeName property is null, then absolutely NO OData type annotation will be written to the payload.
    • If the TypeAnnotation property is not null and the TypeName property is not null` as well, then the string value specified will be written to the payload.

    That said, if you specify a value for the TypeAnnotation property, its TypeName property will override the underlying logic anyway. So please pay attention to the value of the TypeName property.

  • Support multi-NavigationPropertyBindings for a single navigation property

    According to the spec, a navigation property can be used in multiple bindings with different path. It makes a lot of sense for navigation property under complex and containment. From ODataLib V7.0, we are able to support it. The valid format of a binding path is:

    [ ( qualifiedEntityTypeName / qualifiedComplexTypeName ) "/" ] 
                        *( ( complexProperty / complexColProperty) "/" [ qualifiedComplexTypeName "/" ] ) 
                        *( ( containmentNav / colContainmentNav ) "/" [ qualifiedEntityTypeName "/" ] )
                        navigationProperty

    For example, we have containment in Trippin service

    Person
      |—- Trips (containment)
        |—- PlanItems (containment)
          |—- NS.Flight (derived type of PlanItem)
            |—- Airline (navigation property)

    Now we bind entityset Airlines to the property Airline. Since for navigation property binding, we need start from non-containment entityset, then route to navigation property with binding path. So we should have binding:

    <EntitySet Name="People" EntityType="NS.Person">
        <NavigationPropertyBinding Path="Trips/PlanItems/NS.Flight/Airline" Target="Airlines"/>
    </EntitySet>

    If we have InternationalTrips under Person which has type Trip as well, we can have binding <NavigationPropertyBinding Path="InternationalTrips/PlanItems/NS.Flight/Airline" Target="Airlines"/> under People as well. Please note that current Trippin implementation for this part does not have strict compliance with spec due to prohibition of breaking changes.

    Let’s have another example of complex which have multi bindings. City is a navigation property of complex type Address, and Person has HomeAddress, CompanyAddress which are both Address type. Then we can have binding:

    <EntitySet Name="People" EntityType="Sample.Person">
        <NavigationPropertyBinding Path="HomeAddress/City" Target="Cities1" />
        <NavigationPropertyBinding Path="CompanyAddress/City" Target="Cities2" />
    </EntitySet>

    For single binding, binding path is just the navigation property name or type cast appending navigation property name. But for multiple bindings, binding path becames an essential info to create a binding. As a result, following APIs are added:

    EDM

    public EdmNavigationPropertyBinding (IEdmNavigationProperty navigationProperty, IEdmNavigationSource target, IEdmPathExpression bindingPath)
    Use this API to create EdmNavigationPropertyBinding instance if the bindingpath is not navigation property name.

    public void AddNavigationTarget (IEdmNavigationProperty navigationProperty, IEdmNavigationSource target, IEdmPathExpression bindingPath)
    Add a navigation property binding and specify the whole binding path.

    public virtual Microsoft.OData.Edm.IEdmNavigationSource FindNavigationTarget (IEdmNavigationProperty navigationProperty, IEdmPathExpression bindingPath)
    Find navigation property with its binding path.

    ODL

    public ODataQueryOptionParser (IEdmModel model, ODataPath odataPath, String queryOptions)
    public ODataQueryOptionParser(IEdmModel model, ODataPath odataPath, IDictionary<string, string> queryOptions, IServiceProvider container)
    Possibly need ODataPath to resolve navigation target of segments in query option if the navigation property binding path is included in both path and query option. Refer: Navigation property under complex type.

    Take the above complex scenario for example. For generating this kind of model, we need use the new AddNavigationTarget API, and different navigation target can be specified:

    people.AddNavigationTarget(cityOfAddress, cities1, new EdmPathExpression("HomeAddress/City"));
    people.AddNavigationTarget(cityOfAddress, cities2, new EdmPathExpression("Addresses/City"));

    cityOfAddress is the variable to present navigation property City under Address, and cities1 and cities2 are different entityset based on entity type City.

    To achieve the navigation target, the new FindNavigationTarget API can be used:

    IEdmNavigationSource navigationTarget = people.FindNavigationTarget(cityOfAddress, new EdmPathExpression("HomeAddress/City"));
  • Centralized control for OData Simplified Options

    In previous OData library, the control of writing/reading/parsing key-as-segment uri path is separated in ODataMessageWriterSettings/ODataMessageReaderSettings/ODataUriParser. The same as writing/reading OData annotation without prefix. From ODataV7.0, we add a centialized control class for them, which is ODataSimplifiedOptions.

    The following API can be used in ODataSimplifiedOptions:

    public class ODataSimplifiedOptions
    {
        ...
        public bool EnableParsingKeyAsSegmentUrl { get; set; }
        public bool EnableReadingKeyAsSegment { get; set; }
        public bool EnableReadingODataAnnotationWithoutPrefix { get; set; }
        public bool EnableWritingKeyAsSegment { get; set; }
        public bool EnableWritingODataAnnotationWithoutPrefix { get; set; }
        ...
    }

    ODataSimplifiedOptions is registered by DI with singleton ServiceLifetime (Please refer to Dependency Injection support).

  • Read and Write Untyped values in ODataLib

    Starting with ODataLib 7.0, ODataMessageReader & ODataMessageWriter are able to read & write untyped primitive, structured, and collection values.

    Values read from a payload are considered untyped if:

    1. They represent a property not defined in $metadata and do not contain a property annotation
    2. The specified type cannot be resolved against $metadata, or
    3. They are explicitly declared as Edm.Untyped, or Collection(Edm.Untyped)

    The ODataMessageReaderSettings & ODataMessageWriterSettings Validations property can be set with ~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType to enable reading/writing undeclared & untyped value properties in a payload. 

    Untyped values can be read and written as raw strings or, starting with ODataLib 7.3, can be read and written as structured values.

    For compatiblity with ODataLib 7.0, untyped values are read by default as a raw string representing the untyped content. To use the standard OData reader APIs to read untyped content in ODataLib 7.3, set the ODataMessageReaderSettings.ReadUntypedAsString property to false.

    Given the following model:

        
    
        EdmModel model = new EdmModel();
        var entityType = new EdmEntityType("Server.NS", "ServerEntityType", null, false, false, false);
        entityType.AddKeys(entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32));
        model.AddElement(entityType);
    
        var container = new EdmEntityContainer("Server.NS", "Container");
        var entitySet = container.AddEntitySet("EntitySet", entityType);
        model.AddElement(container);

    and the following payload, in which the second property (UndeclaredAddress1) has a type (Server.NS.UndefinedAddress) that is not defined in metadata:

        
    
        InMemoryMessage message = new InMemoryMessage();
        const string payload = @"
        {
            ""@odata.context"":""http://www.sampletest.com/$metadata#EntitySet/$entity"",
            ""Id"":61880128,
            ""UndeclaredAddress1"":
            {
                ""@odata.type"":""Server.NS.UndefinedAddress"",
                ""Street"":""No.999,Zixing Rd Minhang"",
                ""UndeclaredStreet"":""No.10000000999,Zixing Rd Minhang""
            }
        }";
    
        message.Stream = new MemoryStream(Encoding.UTF8.GetBytes(payload));
        message.SetHeader("Content-Type", "application/json");
    
        ODataMessageReaderSettings readerSettings = new ODataMessageReaderSettings
        {
            Validations = ~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType,
            BaseUri = new Uri("http://www.sampletest.com/")
        };

    the following code will read the content of the second property as a raw string.

        
    
        ODataResource entry = null;
        using (var msgReader = new ODataMessageReader((IODataResponseMessage)message, readerSettings, model))
        {
            var reader = msgReader.CreateODataResourceReader(entitySet, entityType);
            while (reader.Read())
            {
                if (reader.State == ODataReaderState.ResourceStart)
    			{
    				entry = reader.Item as ODataResource;
                }
            }
        }
    
        Console.WriteLine(entry.Properties.Count()); // 2
        Console.WriteLine((entry.Properties.Last().Value as ODataUntypedValue).RawValue); 
        // @"{""@odata.type"":""Server.NS.UndefinedAddress"",""Street"":""No.999,Zixing Rd Minhang"",""UndeclaredStreet"":""No.10000000999,Zixing Rd Minhang""}"
        

    By setting ReadUntypedAsString to false, the same content can be read as a structure value:

        
    
        readerSettings.ReadUntypedAsString = false;
    
        ODataResource entry = null;
        ODataResource address = null;
        using (var msgReader = new ODataMessageReader((IODataResponseMessage)message, readerSettings, model))
        {
            var reader = msgReader.CreateODataResourceReader(entitySet, entityType);
            while (reader.Read())
            {
                if (reader.State == ODataReaderState.ResourceStart)
    			{
    	            if (entry == null)
                    {
    					entry = reader.Item as ODataResource;
                    }
                    else
                    {
    					address = reader.Item as ODataResource;
                    }
    			}
            }
        }
    
        Console.WriteLine(entry.Properties.Count()); // 1
        Console.WriteLine(address.Properties.Count()); //2 
        Console.WriteLine(address.TypeAnnotation.TypeName); //"Server.NS.UndefinedAddress" 
        Console.WriteLine(address.Properties.Last().Value); //"No.10000000999,Zixing Rd Minhang" 

    Note that a new reader state, ODataReaderState.Primitive, is added in ODataLib 7.4 in order to support reading primitive values within an untyped collection. Null values within an untyped collection continue to be read as null resources.

    By default, untyped primitive values are returned as boolean if the JSON value is a boolean value, as decimal if the JSON value is numeric, otherwise as string. A custom primitive type resolver can be used in order to return a more specific type based on the value. If the custom type resolver returns null, then the default resolution is applied.

    For example, the following custom primitive type resolver will recognize strings that look like date, time of day, datetimeoffset, duration, and guid values, will differentiate between integer and decimal values, and will resolve “Male” or “Female” as an enum value from the Server.NS.Gender enum type.

    	readerSettings.PrimitiveTypeResolver = CustomTypeResolver;
    
        private static readonly Regex DatePattern = new Regex(@"^(\d{4})-(0?[1-9]|1[012])-(0?[1-9]|[12]\d|3[0|1])$", RegexOptions.Singleline | RegexOptions.Compiled);
        private static readonly Regex TimeOfDayPattern = new Regex(@"^(0?\d|1\d|2[0-3]):(0?\d|[1-5]\d)(:(0?\d|[1-5]\d)(\.\d{1,7})?)?$", RegexOptions.Singleline | RegexOptions.Compiled);
        private static readonly Regex DateTimeOffsetPattern = new Regex(@"^(\d{2,4})-(\d{1,2})-(\d{1,2})(T|(\s+))(\d{1,2}):(\d{1,2})", RegexOptions.Singleline | RegexOptions.Compiled);
        private static readonly Regex DurationPattern = new Regex(@"^P(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(\d+H)?(\d+M)?(\d+(\.\d{1,12})?S)?)?$", RegexOptions.Singleline | RegexOptions.Compiled);
        public static IEdmTypeReference CustomTypeResolver(object value, string typeName)
        {
          string stringValue = value as string;
          if (stringValue != null)
          {
            if (DatePattern.IsMatch(stringValue))
            {
               return EdmCoreModel.Instance.GetDate(/*nullable*/ true);
            }
            if (TimeOfDayPattern.IsMatch(stringValue))
            {
               return EdmCoreModel.Instance.GetTimeOfDay(/*nullable*/ true);
            }
            if (DateTimeOffsetPattern.IsMatch(stringValue))
            {
               return EdmCoreModel.Instance.GetDateTimeOffset(/*nullable*/ true);
            }
            if (DurationPattern.IsMatch(stringValue))
            {
               return EdmCoreModel.Instance.GetDuration(/*nullable*/ true);
            }
            Guid guidResult;
            if (Guid.TryParse(stringValue, out guidResult))
            {
               return EdmCoreModel.Instance.GetGuid(/*nullable*/ true);
            }
            if (stringValue == "Male" || stringValue == "Female")
            {
               return new EdmEnumTypeReference(model.FindDeclaredType("Server.NS.Gender") as IEdmEnumType, /*nullable*/ true);
            }
          }
          if (value is int)
          {
            return EdmCoreModel.Instance.GetInt64(/*nullable*/ true);
          }
         return null;
        }

    To write a raw string into the payload, create an ODataUntypedValue and set the RawValue to the value to be written. Note that there is no validation of the contents of the string written to the payload and it will break clients if it is malformed or if it does not match the expected content-type of the payload.

            new ODataProperty
            {
                Name = "UndeclaredAddress1",
                Value = new ODataUntypedValue()
                {
                    RawValue=@"{""@odata.type"":""#Server.NS.AddressInValid"",'Street':""No.999,Zixing Rd Minhang"",""UndeclaredStreet"":'No.10000000999,Zixing Rd Minhang'}}"
                }
            }    

    As of ODataLib 7.3, it is possible to write primitive, structured, and collection values to an untyped property, or within an untyped collection. A new WritePrimitive() method is added to the ODataWriter for writing an ODataPrimitiveValue within an untyped collection.

    For example, the following writes an untyped collection containing a null, followed by a structured value with a single property, followed by a nested collection containing two primitive values:

                writer.WriteStart(new ODataResourceSet
                {
                    TypeName = "Collection(Edm.Untyped)"
                });
                writer.WritePrimitive(new ODataPrimitiveValue("CollectionMember1"));
                writer.WriteStart((ODataResource)null);
                writer.WriteEnd(); // null resource
                writer.WriteStart(new ODataResource
                {
                    TypeName = "Edm.Untyped",
                    Properties = new[]
                        {
                            new ODataProperty {Name = "NestedResourceId", Value = new ODataPrimitiveValue(1)},
                        }
                });
                writer.WriteEnd(); // nested resource
                writer.WriteStart(new ODataResourceSet());
                writer.WritePrimitive(new ODataPrimitiveValue("NestedCollectionMember1"));
                writer.WritePrimitive(new ODataPrimitiveValue("NestedCollectionMember2"));
                writer.WriteEnd(); // nested resource set
                writer.WriteEnd(); // outer resource set
  • Optional Parameters

    Starting with ODataLib 7.3, parameters can be marked as optional. Optional parameters may be omitted when invoking a custom function.

    Function resolution will first attempt to find an overload that exactly matches the specified function. If there is no exact match, it will select the function overload that matches all specified parameters and for which all required parameters are specified, and fail if there is more than one such function.

    Optional parameters are annotated in CSDL using the new Org.OData.Core.V1.OptionalParameter annotation term:

    "<Parameter Name = "optionalParam" Type="Edm.String">"
       "<Annotation Term="Org.OData.Core.V1.OptionalParameter"/>"
    "</Parameter>"
    

    An optional parameter may optionally specify a default value. The default value is purely informational, and conveys to the consumer the value that will be used if the parameter is not supplied:

     "<Parameter Name = "optionalParamWithDefault" Type="Edm.String">"
        "<Annotation Term="Org.OData.Core.V1.OptionalParameter">"
          "<Record>"
            "<PropertyValue Property="DefaultValue" String="Smith"/>"
           "</Record>"
         "</Annotation>"
      "</Parameter>"
    

    Any optional parameters must be come after all non-optional parameters for a function or action.

    When reading CSDL, optional values are returned as parameters that support IEdmOptionalParameter:

        
    
           foreach (IEdmOperationParameter parameter in function)
           {
             var optional = parameter as IEdmOptionalParameter;
             Console.Write("Parameter '{0}' is {1} ", parameter.Name, optional == null ? "required" : "optional");
    
             if (optional != null && !String.IsNullOrEmpty(optional.DefaultValueString))
             {
               Console.Write(" and has a default value of " + optional.DefaultValueString);
             }
    
             Console.WriteLine();
           }

    When building a model, optional parameters may be specified using the new EdmOptionalParameter class:

        
    
           var stringTypeReference = new EdmStringTypeReference(EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.String), false);
           var function = new EdmFunction("test", "TestFunction", stringTypeReference);
           var requiredParam = new EdmOperationParameter(function, "requiredParam", stringTypeReference);
           var optionalParam = new EdmOptionalParameter(function, "optionalParam", stringTypeReference, null);
           var optionalParamWithDefault = new EdmOptionalParameter(function, "optionalParamWithDefault", stringTypeReference, "Smith");
           function.AddParameter(requiredParam);
           function.AddParameter(optionalParam);
           function.AddParameter(optionalParamWithDefault);
  • Json batch format in ODataLib

    Introduction to JSON Batching

    Overview

    Similar to Windows batch command, OData supports grouping multiple requests together, sending a single HTTP request to OData service, and grouping corresponding responses together before sending back as a single HTTP response. The major benefits for request batching are to reduce client/server round trips and to support atomic operation.

    The batch format supported by OData core libraries is multipart/mime for OData protocol up to v4.0. To accommodate the need for a more developer-friendly format, the new JSON format batching support is added to the latest version of OData protocol v4.01. The JSON format batching format also brings another major benefit of allowing requests inside a batch to be executed in specified orders.

    The OData v4.01 protocol specification can be found on OASIS site. The most current version is: http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#_Toc499805328.

    Details of the JSON Batch format can be found in the OData JSON Format v4.01 specification on the OASIS site.. The most current version is: http://docs.oasis-open.org/odata/odata-json-format/v4.01/odata-json-format-v4.01.html#_Toc499716905

    Sample Batch Request in JSON Format

    Here is one sample batch request in JSON format (unnecessary details are omitted for sake of brevity):

    	https://localhost:9000/$batch
    	User-Agent: Fiddler
    	Authorization: <authz token>
    	Content-Type: application/json
    	Accept: application/json
    	Host: localhost:9000
    	Content-Length: 1234
    
    	{
    	  "requests": [
    		{
    		  "method": "POST",
    		  "atomicityGroup": "g1",
    		  "url": "https://localhost:9000/users",
    		  "headers": {
    			"content-type": "application/json; odata.metadata=minimal; odata.streaming=true",
    			"odata-version": "4.0"
    		  },
    		  "id": "g1-r1",
    		  "body": {"upn": "u1@domain.com", "givenName": "Jon", "surname": "Doe"}
    		},
    		{
    		  "id": "r2",
    		  "method": "PATCH",
    		  "url": "https://localhost:9000/users('u2@domain.com')",
    		  "headers": {
    			"content-type": "application/json; odata.metadata=minimal; odata.streaming=true",
    			"odata-version": "4.0"
    		  },
    		  "body": {"givenName":"Jonx"}
    		},
    		{
    		  "id": "g2-r5",
    		  "atomicityGroup": "g2",
    		  "method": "POST",
    		  "url": "https://localhost:9000/users",
    		  "headers": {
    			"content-yype": "application/json; odata.metadata=minimal; odata.streaming=true",
    			"odata-version": "4.0"
    		  },
    		  "body": {"upn": "u5@domain.com", "givenName": "Jon", "surname": "Doe"}
    		},
    		{
    		  "id": "g2-r6",
    		  "atomicityGroup": "g2",
    		  "method": "POST",
    		  "url": "https://localhost:9000/users",
    		  "headers": {
    			"content-type": "application/json; odata.metadata=minimal; odata.streaming=true",
    			"odata-version": "4.0"
    		  },
    		  "body": {"upn": "u6@domain.com", "givenName": "Jon", "surname": "Doe"}
    		},
    		{
    		  "id": "r8",
    		  "method": "get",
    		  "url": "https://localhost:9000/users",
    		  "headers": {
    			"content-type": "application/json; odata.metadata=minimal; odata.streaming=true",
    			"odata-version": "4.0"
    		  },
    		  "dependsOn": ["g1", "g2", "r2"]
    		}
    	  ]
    	}
    

    Significant attributes for the JSON batch request are as follow:

    • payload is in JSON format, an expansion to the original multipart/mixed format.
    • A JSON batch request has only one top level property named requests, whose value is an array containing multiple requests and multiple atomic groups, each of which can contain multiple requests.
    • Request headers Content-Type and Accept designates the batch request and response format to be JSON batching. For comparison, in multipart/mime format these two headers are Multipart/Mixed with optional content type parameters. These are the header values driving the batch processing while achieving ODL compatibility of two batch formats. Typical use case is that request & response are of the same format, but it should not be a limitation for the ODL Core library batching design, as long as the header values are consistent with the payload content in the request & response.
    • Content-Length header of the batch request specifies the content length of the JSON batch request payload.
    • Requests belonged to same atomic group are denoted by the atomicityGroup attribute having the same value, e.g. “g2”. JSON batch format requires that all requests belonged to same group must be adjacent in the requests array.
    • The dependsOn attribute can be used to specify an array of request prerequisites of this request. Here are the important rules for dependsOn derived from the spec for JSON Batch semantics:
      • If request URL references an entity from another request, id of the corresponding request must also be included;
      • Forward referencing is not allowed;
      • Referencing id of request that is part of a group is not allowed, with the exception that the referenced requests are in the same group of this request.

    In the example above, with JSON batch format, this request specifies the following:

    • Contains two atomic groups g1 and g2;
    • Request g1-r1 needs to be executed before g2-r6, which needs to be before r8;
    • Request r2 can be executed in arbitrary order;
    • Request g2-r5 can be executed either before or after g1, but needs to be executed before r8. Additionally, since g2-r5 and g2-6r belong to the same atomic group g2, execution results need to be roll-backed if either of them failed (service specific implementation).

    Typical JSON Batch Request Creation using OData-Core library

    With user-defined EDM model, user can use ODL-Core library to create JSON batch request directly, similar to Multipart/Mixed batch creation. For example:

    		// The following code snippet generates JSON batch payload into a memory stream. 
    		MemoryStream stream = new MemoryStream();
    		IODataRequestMessage requestMessage = CreateRequestMessage(stream);
    		requestMessage.SetHeader("Content-Type", "application/json");
    		ODataMessageWriterSettings settings = new ODataMessageWriterSettings();
    	
    		using (ODataMessageWriter messageWriter = new ODataMessageWriter(requestMessage, settings))
    		{
    			ODataBatchWriter batchWriter = messageWriter.CreateODataBatchWriter();
    			batchWriter.WriteStartBatch();
    			string createRequestId = "contentId-1";
    	                
    			batchWriter.WriteStartChangeset();
    	                
    			ODataBatchOperationRequestMessage createOperationMessage = batchWriter.CreateOperationRequestMessage(
    				"POST",
    				new Uri(string.Format(CultureInfo.InvariantCulture, "{0}/{1}", serviceDocumentUri, "MySingleton")),
    				createRequestId);
    			using (ODataMessageWriter operationMessageWriter = new ODataMessageWriter(createOperationMessage))
    			{
    				ODataWriter entryWriter = operationMessageWriter.CreateODataResourceWriter();
    				ODataResource entry = new ODataResource();
    	                    
    				// skipped customized data population for initialization here... 
    	                    
    				entryWriter.WriteStart(entry);
    				entryWriter.WriteEnd();
    			}
    	
    			// A PATCH operation that depends on the preceding POST operation.
    			List<string> dependsOnIds = new List<string> { createRequestId };
    			ODataBatchOperationRequestMessage updateOperationMessage = batchWriter.CreateOperationRequestMessage(
    				"PATCH",
    				new Uri(string.Format(CultureInfo.InvariantCulture, "{0}/{1}", serviceDocumentUri, "$" + createRequestId)),
    				"updateReuqestId-1",
    				BatchPayloadUriOption.AbsoluteUri,
    				dependsOnIds);
    			using (ODataMessageWriter operationMessageWriter = new ODataMessageWriter(updateOperationMessage))
    			{
    				var entryWriter = operationMessageWriter.CreateODataResourceWriter();
    				var entry = new ODataResource();
    	                    
    				// skipped customized data population for update here... 
    	                    
    				entryWriter.WriteStart(entry);
    				entryWriter.WriteEnd();
    			}
    	
    			batchWriter.WriteEndChangeset();
    	
    			ODataBatchOperationRequestMessage queryOperationMessage = batchWriter.CreateOperationRequestMessage(
    				"GET",
    				new Uri(string.Format(CultureInfo.InvariantCulture, "{0}/{1}", serviceDocumentUri, "MySingleton")),
    				"readOperationId-1");
    			queryOperationMessage.SetHeader("Accept", "application/json;odata.metadata=full");
    	
    			batchWriter.WriteEndBatch();
    		}
    		// stream contains the JSON batch request payload.
    

    Note that in JSON batch, dependsOnIds needs to include the request Id being referenced in the request’s URL, as required by the JSON batch semantics.

    Typical JSON Batch Request/Response Processing by an OData Service

    An ODataBatchReader can be instantiated to process the batch request as follows:

        ODataMessageReader odataMessageReader = new ODataMessageReader(odataRequestMessage, messageReaderSettings, model);
        ODataBatchReader odataBatchReader = odataMessageReader.CreateODataBatchReader();
    

    Note that batch format is detected during the instantiation of ODataBatchReader from the batch request header. For Content-Type of application/json with optional parameters, an instance of ODataJsonLightBatchReader is created, while an ODataMultipartMixedBatchReader object is created for Multipart/Mixed content type with optional parameters.

        ODataMessageWriter batchResponseMessageWriter = new ODataMessageWriter(odataResponseMessage, odataMessageWriterSettings, model);
        ODataBatchWriter batchWriter = batchResponseMessageWriter.CreateODataBatchWriter();
    

    Similar to the request message writer, the response message writer above also sets up the proper ODataPayloadKind for Batch processing.

    Now, start the basic batch processing with request reading / response writing. For batch processing, the request processing (including reading, dispatching) and response writing (including processing responses for individual request, writing batch response) are interleaving and the whole process is driven by OData service using specific call patterns as described below (common for both formats).

    Typically odataBatchReader is started with initial state, so we can start with creating the response envelop, followed by readings & writings.

        
            batchWriter.WriteStartBatch();
    
            while (batchReader.Read())
            {
                switch (batchReader.State)
                {
                    case ODataBatchReaderState.Operation:
    
                        // Create operation request message
                        ODataBatchOperationRequestMessage operationRequestMessage = batchReader.CreateOperationRequestMessage();
    
                        // Create operation response message
                        IODataResponseMessage responseMessage = batchWriter.CreateOperationResponseMessage(operationRequestMessage.currentContentId);    
    
                        // Request processing begins: including request body reader creation, request parsing & execution according to dependencies specified etc. belonged
                        // to implementation of specific services.
                        if (operationMessage.Method == "PUT")
                        {
                            // Processing request payload.
                            using (ODataMessageReader operationMessageReader = new ODataMessageReader(
                                operationMessage, new ODataMessageReaderSettings(), this.userModel))
                            {
                                ODataReader reader = operationMessageReader.CreateODataResourceReader();
    								
                            // Reader reads data from request payload and kick off service backend processing.
                                ...
                            }
    
                            // Query various attributes for this operation request such as request dependencies and atomic group associated
                            // and combine with specific processing of the service.
                            MyProcessingForExecutionOrders(operationMessage.DependsOnIds);
                            MyProcessingForAtomicTransaction(operationMessage.GroupId);
                            // ...
                            
                            // Response processing begins: including response writing (depends on whether it is associated with an atomic group) etc. to 
                            // write back to the HTTP response belonged to implementation of specific services.
    
                            response.StatusCode = 201;
                            response.SetHeader("Content-Type", "application/json;odata.metadata=none");
                            ...
                            // Response processing ends.
                        }
                        else if (operationMessage.Method == "PATCH")
                        {
                            // Process flow of PATCH method similar to PUT method above.
                            // ...
                        }
                        // additional else if blocks for other HTTP method processs flow come here...
    
                    break;
    
                    case ODataBatchReaderState.ChangesetStart:
    
                        // Request's GroupId can be passed to response writer for correlation of groupId between request & response.												
                        batchWriter.WriteStartChangeset(batchReader.CurrentGroupId);
    
                        // Handle atomic group starting here as needed by implementation of specific services.
                        ...
                        // End of atomic group start processing.
                        break;
    
                    case ODataBatchReaderState.ChangesetEnd:
    
                        batchWriter.WriteEndChangeset();
    
                        // Handle atomic group ending here as needed by implementation of specific services.
                        ...
                        // End of atomic group end processing.
                        break;
                }
            }
            
            batchWriter.WriteEndBatch();
    

    With the introduction of JSON batch, the following public APIs / attributes are available:

    • ODataBatchWriter:
      • public ODataBatchOperationRequestMessage CreateOperationRequestMessage(string method, System.Uri uri, string contentId, BatchPayloadUriOption payloadUriOption, IEnumerable<string> dependsOnIds): This new overload can be used to create operation request message with the additional dependsOnIds that explicitly specify operation dependencies in a JSON batch request. This new overload can also be used for Multipart/Mixed to validate the dependencies. Calling an overload without dependencyIDs in Multipart/Mixed validates any referenced statement results are valid according to the protocol.
      • public void WriteStartChangeset(string): This new overload can be used to specify the atomic group Id when the operation request is created, or the operation response’s atomic group Id correlating to value in the request as required by the OData JSON Batch semantics.
    • ODataBatchReader:
      • public string CurrentGroupId: This returns the current group Id when the atomic group is created. It can be used to create the same group Id on the batch response corresponding to that in the incoming request, as required by the OData JSON Batch semantics. Please note that this is not the case for Multipart/Mixed format where the changeset boundary string in request and response are not correlated.
    • ODataBatchOperationRequestMessage:
      • public string GroupId: This returns the operation request’s group Id. For JSON batch, this is the value specified by the user; For Multipart/Mixed, this is the Id string used to generated the boundary parameter value of the changeset.
      • public List<string> DependsOnIds: This returns a list of prerequisite request ids of this operation request.
        • For JSON batch, if the user-specified DependsOn data contains atomic group Id, the atomic group Id will be flattened into its contained required Ids.
        • For Multipart/Mixed batch, the list is default to be derived implicitly per the protocol for Multipart/Mixed format in the original APIs of ODataBatchWriter.CreateOperationRequestMessage, but it can also be specified explicitly by the user using the new overloaded ODataBatchWriter.CreateOperationRequestMessage API above.

    Notes:

    • Only APIs of synchronous flavor are listed above. For async flavor please refer to the corresponding synchronous API.
    • Atomic group in JSON batch corresponds to changeset in Multipart/Mixed batch.

    Typical JSON Batch Response Processing using OData-Core library

    The processing of JSON batch response is similar to that of Multipart/Mixed batch, as shown in the following code snippet:

    
    		ODataBatchOperationResponseMessage operationMessage = batchReader.CreateOperationResponseMessage();
    		using (ODataMessageReader innerMessageReader = new ODataMessageReader(
    			operationMessage, new ODataMessageReaderSettings(), this.userModel))
    		{
    			ODataReader reader = innerMessageReader.CreateODataResourceReader();
    
    			while (reader.Read()) 
    			{
    				// resource data processing here...
    			}
    		}
    

    Note the using block for inner message reader, which helps disposal of the body content stream of the request created during the response reading. The creation of the response body content stream during response processing, similar to creation of the request body content stream during request processing, enables the parallel processing or dispatching of responses or requests of JSON batch.

  • IN Operator

    Overview

    The IN operator is a new feature in OData 4.01 that enables a shorthand way of writing multiple EQ expressions joined by OR. For example,

    GET /service/Products?$filter=Name eq 'Milk' or Name eq 'Cheese' or Name eq 'Donut'

    can become

    GET /service/Products?$filter=Name in ('Milk', 'Cheese', 'Donut')

    Of the binary expression invoking IN, the left operand must be a single value and the right operand must be a comma-separated list of primitive values or a single expression that resolves to a collection; the expression returns true if the left operand is a member of the right operand.

    Usage

    The IN operator is used in expressions that resolve to a boolean. Common use would be with $filter and it can also be used for $orderby. See test cases for examples of supported scenarios.

    Remarks

    Having successfully parsed the $filter expression, the FilterClause will have an Expression member which can be casted to an InNode object. The members of InNode Left and Right represent SingleValueNode and CollectionNode respectively. From there, it is up to the user to do whatever they would like with the parsed information from the InNode. As shown in the code samples, it may be in the user’s best interest to downcast the SingleValueNode and CollectionNode to subclasses to access additional properties.

  • Resource (Complex & Entity) Value

    1. Introduction

    Abstract

    ODataComplexValue is widely used in OData libraries v5.x and v6.x. However, it’s removed in OData library v7.x because complex type should support the navigation property. We should treat the complex same as the entity.

    So, the main changes OData v7.x design are:

    • Remove ODataComplexValue
    • Rename ODataEntry as ODataResource, use that to represent the instance of entity and complex.
    • Rename ODataFeed as ODataResourceSet, use that to represent the instance of collection of entity or complex.

    Problems

    Along with more and more customers upgrade from ODL v6.x to ODL v7.x, many customers find it’s hard to use the library without the ODataComplexValue. Because most of OData customers:

    • Don’t need navigation property on complex type.
    • Can’t convert the instance of entity or complex easily from literal or to literal. Like the Json.Net
    • Can’t create the instance annotation with “complex or entity, or collection of them”
    • Can’t write or read the top-level resource
    • Can’t write or read the top-level property with resource or collection of resource value
    • Can’t read the parameter resource or collection of resource value directly

    2. Proposal

    Current structure

    Below is the main inheritance of the ODataValue vs ODataItem in ODL v6.x.

    Below is the main inheritance of the ODataValue vs ODataItem in ODL v7.x.

    The main changes from 6.x to 7.x is:

    • ODataComplexValue is removed.
    • ODataValue is derived from ODataItem.

    Proposal structure

    We should introduce a new class named ODataResourceValue derived from ODataValue, same as:

    3. Main Works

    ODataResourceValue class

    We will introduce a new class named ODataResourceValue, it should look like (Same as ODataComplexValue):

    public sealed class ODataResourceValue : ODataValue
    {
      public string TypeName { get; set; }
      public IEnumerable<ODataProperty> Properties { get; set; }
      public ICollection<ODataInstanceAnnotation> InstanceAnnotations { get;set; }
    }

    Where:

    • TypeName: save the resource type name.
    • Properties: save the all properties, include the property with resource value or collection of resource value.
    • InstanceAnnotations: save the instance annotations for this resource value.

    ODataCollectionValue class

    We don’t need to change anything for ODataCollectionValue, because it also supports to have ODataResourceValue as its element.

    ODataProperty class

    We don’t need to change anything for ODataProperty, because it also supports to create ODataProperty with ODataResourceValue or ODataCollectionValue.

    ODataResource class

    We don’t need to change anything in ODataResource except to verify the properties don’t include any property whose value is an ODataResourceValue or Collection of ODataResourceValue.

    public class ODataResource
    {
       ...
       public IEnumerable<ODataProperty> Properties
       {
          get { return this.MetadataBuilder.GetProperties(this.properties); }
          set
          {
              // TODO: Add validation here. It's not allowed a property with value as "ODataResourceValue" or a collection of "ODataResourceValue"
              this.properties = value;
          }
        } 
    }

    ODataResourceSet class

    We don’t need to change anything in ODataResourceSet.

    Write ODataResourceValue

    Write in value writer

    Write ODataResourceValue

    We should add a new method named WriteResourceValue(…) to write an ODataResourceValue in ODataJsonLightValueSerializer.

    public void WriteResourceValue(ODataResourceValue resourceValue, )
    {
        // TODO
    }

    This method is the key method in writing scenario.

    Write ODataCollectionValue with ODataResourceValue

    We should update WriteCollectionValue(...) method to call above WriteResourceValue(...) if the item is an ODataResourceValue.

    foreach (object item in items)
    {
         ODataResourceValue itemAsResourceValue = item as ODataResourceValue;
         if (itemAsResourceValue != null)
         {
            this.WriteResourceValue();
         }
         else
         {
            // primitive, enum
         }
    }

    Write in property Writer

    We should update ODataJsonLightPropertyWriter.cs to support writing the property with ODataResourceValue or collection of resource value. This change should in WriteProperty(...) method, it supports to write top-level property and non-top-level property (Nested Property).

    private void WriteProperty()
    {
       ......
       ODataResourceValue resourceValue = value as ODataResourceValue;
       if (resourceValue != null)
       {
           this.JsonWriter.WriteName(propertyName);
           this.JsonLightValueSerializer.WriteResourceValue(resourceValue, );
           return;
        }
    }

    We don’t need to change any codes for property with value as ODataCollectionValue which element is ODataResourceValue.

    Write in instance annotation writer

    We should update JsonLightInstanceAnnotationWriter.cs to support:

    Write the ODataInstanceAnnotation with value as ODataResourceValue

    We should add the below codes in WriteInstanceAnnotation(...) method by calling the WriteResourceValue(...) if the instance annotation value is an ODataResourceValue.

    ODataResourceValue resourceValue = value as ODataResourceValue;
    if (resourceValue != null)
    {
       this.WriteInstanceAnnotationName(propertyName, name);
       this.valueSerializer.WriteResourceValue(resourceValue,);
       return;
    }

    Write the ODataInstanceAnnotation with value as collection of ODataResourceValue.

    We don’t need to do anything. Because it supports to write the ODataCollectionValue, which in turns will call WriteResourceValue() for each ODataResourceValue elements.

    Write in collection writer

    We should update ODataJsonLightCollectionWriter.cs to support write collection with ODataResourceValue item.

    protected override void WriteCollectionItem(object item, IEdmTypeReference expectedItemType)
    {
        ODataResourceValue resourceValue = item as ODataResourceValue;
        if (resourceValue != null)
        {
            this.jsonLightCollectionSerializer.WriteResourceValue(resourceValue,);
        }
    
    }

    Write in parameter writer

    We should update ODataJsonLightParameterWriter.cs to support write resource or collection of resource value.

    protected override void WriteValueParameter(string parameterName, object parameterValue, IEdmTypeReference expectedTypeReference)
    {
        ......
        ODataResourceValue resourceValue = parameterValue as ODataResourceValue;
        if (resourceValue != null)
        {
            this.jsonLightValueSerializer.WriteResourceValue(resourceValue, .);                      
        }
    }

    TBD: Normally, if you want to write a Collection parameter, you should do:

    var parameterWriter = new ODataJsonLightParameterWriter(outputContext, operation: null);
    parameterWriter.WriteStart();
    var collectionWriter = parameterWriter.CreateCollectionWriter("collection");
            collectionWriter.WriteStart(new ODataCollectionStart());
              collectionWriter.WriteItem("item1");
            collectionWriter.WriteEnd();
     parameterWriter.WriteEnd();

    However, i think we should support to write the collection value directly if customer call WriteValueParameter() method with the ODataCollectionValue.

    Basically, we don’t need to change any codes for the “Collection parameter value” writer. Customer still can use “CreateCollectionWriter” to write the collection with more information.

    Besides, We don’t need to change any codes for Resource or ResourceSet parameter writer. Customer still can use them to writer ODataResource or ODataResourceSet one by one. See:

    • CreateFormatResourceWriter
    • CreateFormatResourceSetWriter

    Convert ODataResourceValue to Uri literal

    We should support to convert ODataResourceValue and collection of it to Uri literal when customer call ConvertToUriLiteral(...) in ODataUriUitl.cs.

    public static string ConvertToUriLiteral(object value, ODataVersion version, IEdmModel model)
    {
       ......
    	 ODataResourceValue resourceValue = value as ODataResourceValue;
       if (resourceValue != null)
       {
          return ODataUriConversionUtils.ConvertToResourceLiteral(resourceValue, model, version);
       }
    
       ......
    }

    Read ODataResourceValue

    Read ODataResourceValue in value reader

    We should update ODataJsonLightPropertyAndValueDeserialier.cs to read the resource value.

    private ODataResourceValue ReadResourceValue(
                bool insideJsonObjectValue,
                bool insideComplexValue,
                string propertyName,
                IEdmStructuredTypeReference structuredTypeReference,
                string payloadTypeName,
                PropertyAndAnnotationCollector propertyAndAnnotationCollector)
    {}

    This method is the key method in reading scenario, it should support to:

    • Read its own instance annotation
    • Read all properties value, include nested resource value.

    The above method is called from:

    private object ReadNonEntityValueImplementation()
    {
        ......
    
        case EdmTypeKind.Complex: // nested complex
        case EdmTypeKind.Entity: // nested entity
             ......
    	 result = ReadResourceValue(......);
    	 break;
    }

    For the collection of resource, owing that ReadCollectionValue() will call ReadNonEntityValueImplemenation(…) to read its elements, so, if the item is entity or complex, it will return DataResourceValue. We don’t need to change any codes.

    Read ODataResourceValue in instance annotation reader

    ODataJsonLightPropertyAndValueDeserialier.cs has the following method to read instance annotation value:

    internal object ReadCustomInstanceAnnotationValue (PropertyAndAnnotationCollector propertyAndAnnotationCollector, string name)
    {
    object customInstanceAnnotationValue = this.ReadNonEntityValueImplementation();
    }

    So, we don’t need to change any codes for it.

    Read ODataResourceValue in collection reader

    ODataJsonLightCollectionDeserializer.cs will call ReadNonEntityValueImplementation. We don’t need change any code.

    However, there’s some validation codes that need to change.

    Read ODataResourceValue as OData error value

    ODataJsonLightErrorDeserializer.cs will call ReadNonEntityValueImplementation. We don’t need change any code.

    Read in Resource deserializer

    ODataJsonLightResourceDeserializer.cs will call ReadNonEntityValueImplementation to read the value of its property. However,

    • It is not used to read the “complex and collection of complex”
    • It is not used to read the “navigation property”
    • It’s ONLY used it to read the primitive, enum and collection of them. And, for the “complex and collection of complex”, we still create nested resource info. So, we don’t need to change anything.

    Read resource in parameter

    ODataJsonLightParameterDeserialzer.cs is used to read parameter value. So far, for the entity, complex, it only returns a parameter state as “Resource”, for the collection of them, return a parameter state of “ResourceSet” as below:

    • Primitive type, read as primitive value.
    • Enum type, read as “ODataEnumValue”
    • TypeDefintion, read as “TypeDefinition” value
    • Complex, Entity, read nothing, just return “ReaderState.Resource”
    • Collection,
      • If element is primitive, read as primitive value.
      • If element is enum, read nothing, just return “ReaderState.Collection”
      • If element is complex, entity, read nothing, just return “ReaderState.ResourceSet”. So, we should have a configuration enable customer to change the logic. For example: On ODataMessageReader, we can enable customer to create a parameter reader which can read all parameter as value.
    public ODataParameterReader CreateODataParameterReader(IEdmOperation operation, bool readAllAsValue)
    {
      ......
    }

    So, if customer call the above method using true for readAllAsValue, he can get:

    • Complex, Entity, read as “ODataResourceValue”
    • Collection, read as “ODataCollectionValue”.

    Read Top-Level Property

    ODataJsonLightPropertyAndValueDeserializer.cs can read the top-level property into a ODataProperty. So, we can read a top-level complex, entity, or collection or complex, entity property.

    Convert ODatResourceValue from Url literal

    We should convert the ODataResourceValue from JSON Uri literal in ConvertFromUriLiteral(...) in ODataUriUtils.cs.

    4. Open Questions

    What’s the string output if convert a null “ODataCollectionValue”?

    So far, if you create:

    ODataCollectionValue value = null;
    String str = ConvertToUriLiteral(value, ODataVersion.V4, model);
    Assert.Equal("null", str); // true?

    However, it should be “[]” ?

    Do we write the instance annotation if call ConvertToUriLiteral?

    In the 6.x version, if a complex value has instance annotations, those instance annotations will not write out when we call like:

    ODataComplexValue  value = new ODataComplexValue()
    {
           TypeName = "TestModel.Address",
           Properties = new ODataProperty[] { new ODataProperty() { Name = "Street", Value = "street" }} ,
           InstanceAnnotations = new[]
               {
                       new ODataInstanceAnnotation("Custom.Ok", new ODataComplexValue
                         {
                              TypeName = "TestModel.Address",
                              Properties = new ODataProperty[] { new ODataProperty() { Name = "Street", Value = "street" }})
                            }
               }
    
    string str = ODataUriUtils.ConvertToUriLiteral(value, ODataVersion.40, model);

    Where, str doesn’t include the instance annotation? But, we should include the instance annotation.

6. DESIGN

  • 6.1 IN Operator Design

    IN Operator

    OData Specification

    The IN operator is a new feature in OData 4.01 that enables a shorthand way of writing multiple EQ expressions joined by OR. For example,

    GET /service/Products?$filter=Name eq 'Milk' or Name eq 'Cheese' or Name eq 'Donut'

    can become

    GET /service/Products?$filter=Name in ('Milk', 'Cheese', 'Donut')

    Of the binary expression invoking IN, the left operand must be a single value and the right operand must be a comma-separated list of primitive values or a single expression that resolves to a collection; the expression returns true if the left operand is a member of the right operand.

    Syntax

    Augmented Backus-Naur Form (ABNF)

    Per the ABNF, the IN operator is part of the common expression syntax

    commonExpr = ( primitiveLiteral
                 / arrayOrObject
                 / rootExpr
                 / firstMemberExpr
                 / functionExpr
                 / negateExpr
                 / methodCallExpr
                 / parenExpr
                 / listExpr
                 / castExpr
                 / isofExpr
                 / notExpr
                 )
                 [ addExpr
                 / subExpr
                 / mulExpr
                 / divExpr
                 / divbyExpr
                 / modExpr
                 ]
                 [ eqExpr
                 / neExpr
                 / ltExpr
                 / leExpr
                 / gtExpr
                 / geExpr
                 / hasExpr
                 / inExpr
    

    Test cases explicitly outlined by the ABNF spec include the following (see Scenarios for details on some of these types of queries):

    <TestCase Name="5.1.1.1.12 Logical Operator Examples" Rule="boolCommonExpr">
      <Input>Name in ('Milk', 'Cheese')</Input>
    </TestCase>
    <TestCase Name="5.1.1.1.12 Logical Operator Examples" Rule="boolCommonExpr">
      <Input>Name in ["Milk", "Cheese"]</Input>
    </TestCase>
    

    Design Strategy

    Uri Query Expression Parser

    The logic to parse expressions for query options are written in the UriQueryExpressionParser class (src\Microsoft.OData.Core\UriParser\Parsers\UriQueryExpressionParser.cs). The algorithm tokenizes the expression by whitespaces and builds QueryTokens from the operands. If more than one operand exists, then the algorithm recursively builds the right operand as a tree.

    There exists a code flow for generating binary operator tokens (used for eq, ne, gt, ge, has, etc.), which would make sense for IN to follow. However, the binary operator tokens are eventually converted to binary operator nodes, where the left and right nodes must be single value nodes; the binary operator node does not suit the IN operator, which has a left single-valued operand and right collection-valued operand. Therefore, we will create separate QueryToken, MetadataBinder, and SingleValueNode classes to accommodate IN but maintain a similar code flow as the binary operator.

    Additionally, we will need to ensure that the expression parser can read static collections (i.e. parentheses-enclosed members), so we will derive from the CollectionNode class to represent the static collection.

    QueryToken Class

    We will need to derive a separate class for IN scenarios. The code path looks similar to the BinaryOperatorToken construction but the algorithm will result in an InToken, which holds different information from the BinaryOperatorToken. See src\Microsoft.OData.Core\UriParser\Parsers\UriQueryExpressionParser.cs : ParseComparison() for code path.

    /// <summary>
    /// Base class for all lexical tokens of OData query.
    /// </summary>
    public abstract class QueryToken 
    {
        /// <summary>
        /// Empty list of arguments.
        /// </summary>
        public static readonly QueryToken[] EmptyTokens = new QueryToken[0];
    
        /// <summary>
        /// The kind of the query token.
        /// </summary>
        public abstract QueryTokenKind Kind { get; }
    
        /// <summary>
        /// Accept a <see cref="ISyntacticTreeVisitor{T}"/> to walk a tree of <see cref="QueryToken"/>s.
        /// </summary>
        /// <typeparam name="T">Type that the visitor will return after visiting this token.</typeparam>
        /// <param name="visitor">An implementation of the visitor interface.</param>
        /// <returns>An object whose type is determined by the type parameter of the visitor.</returns>
        public abstract T Accept<T>(ISyntacticTreeVisitor<T> visitor);
    }
    

    SingleValueNode Class

    The InNode will derive from the SingleValueNode class and will have similarities with the BinaryOperatorNode. The InNode will have a SingleValueNode and CollectionNode.

    /// <summary>
    /// Base class for all semantic metadata bound nodes which represent a single composable value.
    /// </summary>
    public abstract class SingleValueNode : QueryNode
    {
        /// <summary>
        /// Gets the type of the single value this node represents.
        /// </summary>
        public abstract IEdmTypeReference TypeReference
        {
            get;
        }
    
        /// <summary>
        /// Gets the kind of this node.
        /// </summary>
        public override QueryNodeKind Kind
        {
            get { return (QueryNodeKind)this.InternalKind; }
        }
    }
    

    Static Collections with Parentheses/Brackets

    The expression parser must also recognize static collections enclosed by parentheses or brackets. In these instances, the parser must create a collection with such objects. Therefore we will create a new CollectionConstantNode, derived from CollectionNode, that represents a list of ConstantNodes.

    /// <summary>
    /// Base class for all semantic metadata bound nodes which represent a composable collection of values.
    /// </summary>
    public abstract class CollectionNode : QueryNode
    {
        /// <summary>
        /// The resouce type of a single item from the collection represented by this node.
        /// </summary>
        public abstract IEdmTypeReference ItemType
        {
            get;
        }
    
        /// <summary>
        /// The type of the collection represented by this node.
        /// </summary>
        public abstract IEdmCollectionTypeReference CollectionType
        {
            get;
        }
    
        /// <summary>
        /// Gets the kind of this node.
        /// </summary>
        public override QueryNodeKind Kind
        {
            get { return (QueryNodeKind)this.InternalKind; }
        }
    }
    
  • 6.2 Design doc for $expand in context-url for OData V4.01

    $expand in Context Url

    Introduction

    In form of metadata, context url in response from OData-compliant service provides a way to describe the response payload, and is used as control information to facilitate response processing by the client.

    Here is an example of a simple query request and resulting response containing context url:

    Request url: http://services.odata.org/V4/TripPinService/People(‘russellwhyte’)?$select=FirstName,LastName

    Response:

    {
        "@odata.context": "http://services.odata.org/V4/TripPinService/$metadata#People(FirstName,LastName)/$entity",
        "@odata.id": "http://services.odata.org/V4/TripPinService/People('russellwhyte')",
        "@odata.etag": "W/\"08D62407F53DE9ED\"",
        "@odata.editLink": "http://services.odata.org/V4/TripPinService/People('russellwhyte')",
        "FirstName": "Russell",
        "LastName": "Whyte"
    }
    

    As shown above, @odata.context annotation specifies the context url, and provides the following machine-readable description for response data:

    • It is regarding an entity from the “People” entity set,
    • It contains two property values “FirstName”, “LastName”.

    Expanded Entity Specification for OData V4.01

    OData V4.0 specifies that expanded entities with nested selects are included in the context Url as the name of the navigation property suffixed with the comma separated list of selected properties, enclosed in parens. OData V4.01 format specifies that, in the absence of any nested selects, the expanded navigation property appears suffixed with empty parens. This is distinct, and may appear in addition to, the un-suffixed navigation property name, which indicates that the navigation property appears in the $select list (indicating that the navigationLink should be included in the response).

    10.10 Expanded Entity
    Context URL template:
    
    {context-url}#{entity-set}{/type-name}{select-list}/$entity
    
    {context-url}#{singleton}{select-list}
    
    {context-url}#{type-name}{select-list}
    
    For a 4.01 response, if a navigation property is explicitly expanded, then in addition to the non-suffixed names of any selected properties, navigation properties, functions or actions, the comma-separated list of properties MUST include the name of the expanded property, suffixed with the parenthesized comma-separated list of any properties of the expanded navigation property that are selected or expanded. If the expanded navigation property does not contain a nested $select or $expand, then the expanded property is suffixed with empty parentheses. [If the expansion is recursive for nested children, a plus sign (+) is infixed between the navigation property name and the opening parenthesis.]
    
    For a 4.0 response, the expanded navigation property suffixed with parentheses MAY be omitted from the select-list if it does not contain a nested $select or $expand, but MUST still be present, without a suffix, if it is explicitly selected.
    

    Change Summary

    According to the OData spec above, this change is for creating proper expand token in response’s context url, corresponding to $expand clause in the request url. Context Url is used as response metadata to control data materialization when response is received by the client. We need to make sure that:

    • Context url in the response generated contains correct token for the $expand clause.
    • Client can parse the context url correctly and uses the metadata for response processing.

    V4.01

    As required, we are going to add to the context url the parenthesized (either empty or non-empty) navigation property when it is expanded. For example:

    Request Url Context Url in Response
    root/Cities(‘id’)?$expand=TestModel.CapitalCity/Districts root/$metadata#Cities(TestModel.CapitalCity/Districts())/$entity
    root/Cities(‘id’)?$expand=TestModel.CapitalCity/Districts($select=DistrictName) root/$metadata#Cities(TestModel.CapitalCity/Districts(DistrictName))/$entity
    root/Cities(‘id’)?$select=Name&$expand=TestModel.CapitalCity/Districts($select=DistrictName) root/$metadata#Cities(Name,TestModel.CapitalCity/Districts(DistrictName))/$entity
    root/Cities(‘id’)?$select=Name,TestModel.CapitalCity/Districts&$expand=TestModel.CapitalCity/Districts($select=DistrictName) root/$metadata#Cities(Name,TestModel.CapitalCity/Districts,TestModel.CapitalCity/Districts(DistrictName))/$entity

    We also fix the known issue https://github.com/OData/odata.net/issues/1104 when a navigation link is both expanded and selected, the parenthesized navigation property will be present in the context-url, along with the navigation link explicitly selected:

    Request Url Context Url in Response
    root/Cities(‘id’)?$select=TestModel.CapitalCity/Districts&$expand=TestModel.CapitalCity/Districts root/$metadata#Cities(TestModel.CapitalCity/Districts,TestModel.CapitalCity/Districts())/$entity
    root/Cities(‘id’)?$select=Id,Name,ExpandedNavProp&$expand=ExpandedNavProp root/$metadata#Cities(Id,Name,ExpandedNavProp,ExpandedNavProp())/$entity

    V4.0

    We are going to tweak the behavior to align with that of the 4.01. While this change does introduce slightly different semantics for explicitly selected navigation property, it is not considered a breaking change because the updated context-url form for expanded navigation link is legal, but just was not required. It has been confirmed through code examination and ODL tests that this change doesn’t cause anomalies for libraries and client.

    Context Url Parsing Updates to Align with OData V4/V4.01 ABNF Spec

    Notion of expanded entity in context url has been introduced since OData V4 ABNF. Here is the excerpt of context url select clause in OData V4.01 ABNF:

    ;------------------------------------------------------------------------------
    ; 3. Context URL Fragments
    ;------------------------------------------------------------------------------
    
    context         = "#" contextFragment
    contextFragment = 'Collection($ref)'
                    / '$ref'
                    / 'Collection(Edm.EntityType)'
                    / 'Collection(Edm.ComplexType)'
                    / singletonEntity [ navigation *( containmentNavigation ) [ "/" qualifiedEntityTypeName ] ] [ selectList ]
                    / qualifiedTypeName [ selectList ]
                    / entitySet ( '/$deletedEntity' / '/$link' / '/$deletedLink' )
                    / entitySet keyPredicate "/" contextPropertyPath [ selectList ]
                    / entitySet [ selectList ] [ '/$entity' / '/$delta' ]
                    
    entitySet = entitySetName *( containmentNavigation ) [ "/" qualifiedEntityTypeName ]
                
    containmentNavigation = keyPredicate [ "/" qualifiedEntityTypeName ] navigation
    navigation            = *( "/" complexProperty [ "/" qualifiedComplexTypeName ] ) "/" navigationProperty   
    
    selectList         = OPEN selectListItem *( COMMA selectListItem ) CLOSE
    selectListItem     = STAR ; all structural properties
                       / allOperationsInSchema 
                       / [ qualifiedEntityTypeName "/" ] 
                         ( qualifiedActionName
                         / qualifiedFunctionName 
                         / selectListProperty
                         )
    selectListProperty = primitiveProperty  
                       / primitiveColProperty 
                       / navigationProperty [ "+" ] [ selectList ]
                       / selectPath [ "/" selectListProperty ]
    
    contextPropertyPath = primitiveProperty
                        / primitiveColProperty
                        / complexColProperty
                        / complexProperty [ [ "/" qualifiedComplexTypeName ] "/" contextPropertyPath ]
    

    The expanded entity introduced nested list of comma-separated items for the projected expanded entity. While expanded entity is supported at high level per OData V4 Spec, some common cases such as projecting multiple properties in expanded entities, e.g. $expand=Account($select=UPN,DisplayName), are not supported by context Url parsing implementation, which currently is aligned with only ABNF V3.

    This requires updating the context URL selected clause parsing implementation to align with ABNF V4.01. Issue #1268 is added to the scope of this task.

    Design

    1. In OData 6.x & 7.x implementation, while creation of the context url select clause is aligned with OData V4/V4.01 ABNF spec, its counterpart of lexical parsing is still using OData ABNF V3, which specifies context url select list as a flat list of comma-separated items. To align with OData ABNF V4.01, parser needs to adopt a recursive approach. In addition to the path segments representing simple properties originally, a notion of expanded entity needs to be introduced in SelectedPropertiesNode to accommodate requirements from OData ABNF V4.

    2. During response de-serialization, the emitted expanded navigation property token (in the form of parenthesized navigation link) could cause incompatibility/ambiguity to the semantics of context-url’s selected-list, where an empty list stands for select-all(entire subtree). This is actually an issue in current library which always emits “Projected Expanded Entity” into the selected list. It could cause inadvertent omission of navigation link information during materialization (see issue #1259) in current library, and needs to be addressed as a prerequisite of this task. . Check whether the select clause during SelectPropretiesNode instantiation only contains expanded navigation link, which contains balanced parentheses. The item should also be resolvable to navigation property, not other types of property. . Additional EDM type information needs to be passed in for navigation property resolution in the constructor of the SelectedPropertiesNode. The EDM type information is originated from the OData reader, which instantiates the SelectedPropertiesNode.

    3. For both V4 and V4.01, always emit the parenthesized navigation link during response writing for context-url.

    4. For internal logic of combining select list and expand list together, we should, according to select list’s semantics, create a node of entire subtree when the selected list is empty.

    5. Cleanup the callback function processSubResult signatures by removing ODataVersion argument and associated method, since same logic is used for both V4 and V4.01. The callback function is used for traversing the select&expand clause recursively and providing aggregated result.

      private static string ProcessSubExpand(string expandNode, string subExpand, ODataVersion version)
      
    private static SelectedPropertiesNode ProcessSubExpand(string nodeName, SelectedPropertiesNode subExpandNode, ODataVersion version)
    

    Work Items Breakdown:

    Task | Estimate ——| ——– Research on OData spec for v4.01 related to context-url and projected expand entity | Completed Research on OData implementation related to context-url serialization / de-serialization / metadata level | Completed Address prerequisite issue of navigation link materialization for expand-sub-select clause combined with minimal metadata | Completed (Code Review pending) Design documentation | Completed Implementation of $expand in context url for V4.01: based on PoC done during research | 3 days (WIP, pending on Design Review) Implementation of context url select clause parsing per OData V4.01 ABNF spec. Feature doc | 1 day

    Note: The above estimates include test pass for the initial implementation. Code review cycles are not included.

    Tracking:

    • Prerequisite issue/PR: https://github.com/OData/odata.net/issues/1259; https://github.com/OData/odata.net/pull/1264
    • “$expand for context-url” issue/PR: https://github.com/OData/odata.net/issues/1265;