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.

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)

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

  • Support undeclared & untyped property value in ODataLib

    From ODataLib 7.0, ODataMessageReader & ODataMessageWriter are able to read & write arbitrary JSON as raw string in the payload. The undeclared & untyped property is supported by ODataLib in a slightly different way than that in ODataLib 5.x.

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

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

    The below messageWriterSettings will enable reading undeclared/untyped property. It reads an untype JSON as ODataUntypedValue. And ODataUntypedValue.RawJson has the raw JSON string.

        
    
        InMemoryMessage message = new InMemoryMessage();
        const string payload = @"
        {
            ""@odata.context"":""http://www.sampletest.com/$metadata#EntitySet/$entity"",
            ""Id"":61880128,
            ""UndeclaredAddress1"":
            {
                ""@odata.type"":""Server.NS.AddressInValid"",
                '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/")
        };
    
        ODataResource entry = null;
        using (var msgReader = new ODataMessageReader((IODataResponseMessage)message, readerSettings, model))
        {
            var reader = msgReader.CreateODataResourceReader(entitySet, entityType);
            while (reader.Read())
            {
                entry = reader.Item as ODataResource;
            }
        }
    
        Console.WriteLine(entry.Properties.Count()); // 2
        // @"{""@odata.type"":""Server.NS.AddressInValid"",""Street"":""No.999,Zixing Rd Minhang"",""UndeclaredStreet"":""No.10000000999,Zixing Rd Minhang""}"
        Console.WriteLine((entry.Properties.Last().Value as ODataUntypedValue).RawValue); 
        

    And this is how to write undeclared & untyped property:

    ![](57)
        MemoryStream outputStream = new MemoryStream();
        IODataResponseMessage message = new InMemoryMessage() { Stream = outputStream };
        message.SetHeader("Content-Type", "application/json;odata.metadata=minimal");
    
        ODataMessageWriterSettings writerSettings = new ODataMessageWriterSettings
        {
            Validations = ~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType,
            ODataUri = new ODataUri()
            {
                ServiceRoot = new Uri("http://www.sampletest.com/"),
            };
        };
    
        var entry = new ODataResource
        {
            TypeName = "Server.NS.ServerEntityType",
            Properties = new[]
                {
            new ODataProperty{Name = "Id", Value = new ODataPrimitiveValue(61880128)},
            new ODataProperty{Name = "UndeclaredFloatId", Value = new ODataPrimitiveValue(12.3D)},
            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'}}"
                }
            },
                }
        };
    
        using (var msgWriter = new ODataMessageWriter((IODataResponseMessage)message, writerSettings, model))
        {
            var writer = msgWriter.CreateODataResourceWriter(entitySet, entityType);
    
            writer.WriteStart(entry);
            writer.WriteEnd();
        }