1. INTRODUCTION

  • 1.1 Preface

    About

    This document aims to provide organized content about how to build a OData V4 service using ASP.NET Web API for OData. It’s more than just a getting started guidance and is more systematic than the samples.

    Version

    This is the first version of this document written in April, 2015.

    Targetted audience

    This document fits best the readers who has a relative good knowledge of OData (e.g. knowing the OData primitive and structured types, knowing the basic OData URL conventions, knowing the basic OData features such as operations, queries and so on) and would like to explore how some advanced scenarios can be implemented using Web API for OData.

    Beginners to OData or Web API for OData can also leverage this document as a structured way to learn. But it’s strongly recommended to read the Getting Started tutorials on OData.org to get a grasp of OData concepts before reading this doc.

    This document also assumes that the readers know how to create projects in Visual Studio and know how to install packages using the Nuget Package Manager. It also assumes they have knowledge in C# programming and are not unfamiliar with concepts like classes, properties, methods, and so on.

    Structure of this document

    This document starts with a tutorial about how a simplest OData V4 service can be written using ASP.NET Web API for OData. Then it steps into the section about how OData models can be built in different ways. After that, OData routing is introduced in details followed by a description of OData feature implementation. Finally, it talks about security and customization of the OData V4 service.

    Resources and references

  • 1.2 Write a simple OData V4 service

    Let’s get started by creating a simple OData V4 service. It has one entity set Products, one entity type Product. Product has two properties ID and Name, with ID being an integer and Name being a string. The service is read only. The only data clients can get besides the service document and metadata document, is the Products entity set.

    a. Create the Visual Studio project

    In Visual Studio, create a new C# project from the ASP.NET Web Application template. Name the project “ODataService”.

    In the New Project dialog, select the Empty template. Under “Add folders and core references…”, click Web API. Click OK.

    b. Install the OData packages

    In the Nuget Package Manager, install Microsoft.AspNet.OData and all it’s dependencies.

    c. Add a model class

    Add a C# class to the Models folder:

    namespace ODataService.Models
    {
        public class Product
        {
            public int ID { get; set; }
            public string Name { get; set; }
        }
    }

    d. Add a controller class

    Add a C# class to the Controllers folder:

    namespace ODataService.Controllers
    {
        public class ProductsController : ODataController
        {
            private List<Product> products = new List<Product>()
            {
                new Product()
                {
                    ID = 1,
                    Name = "Bread",
                }
            };
    
            public List<Product> Get()
            {
                return products;
            }
        }
    }

    In the controller, we defined a List<Product> object which has one product element. It’s considered as a in-memory storage of the data of the OData service.

    We also defined a Get method that returns the list of products. The method refers to the handling of HTTP GET requests. We’ll cover that in the sections about routing.

    e. Configure the OData Endpoint

    Open the file App_Start/WebApiConfig.cs. Replace the existing Register method with the following code:

    public static void Register(HttpConfiguration config)
    {
        var builder = new ODataConventionModelBuilder();
    
        builder.EntitySet<Product>("Products");
    
        config.MapODataServiceRoute("ODataRoute", null, builder.GetEdmModel());
    }

    f. Start the OData service

    Start the OData service by running the project and open a browser to consume it. You should be able to get access to the service document at http://host/service/ in which http://host/service/ is the root path of your service. The metadata document can be accessed at GET http://host/service/$metadata and the products at GET http://host/service/Products.

2. DEFINING THE MODEL

  • 2.1 Introduction to the model builders

    The data model is the basis of an OData service. OData service uses an abstract data model called Entity Data Model (EDM) to describe the exposed data in the service. OData client can issue a GET request to the root URL of the OData service with $metadata to get an XML representation of the service’s data model. In Microsoft ASP.NET Web API 2.2 for OData v4.0, to build a data model for OData service is to create an IEdmModel object. There are three ways to build an EDM model in Web API OData:

    1. Explicit Edm Model Builder
    2. Implicit, non-convention model builder or fluent API
    3. Implicit, convention model builder.

    2.1.1 Build Edm Model

    Let’s see the difference between them.

    Explicit model builder

    To build an Edm model explicitly is to create an IEdmModel object by directly using APIs in ODatalib. The basic code structure to build an Edm model explicitly is shown as:

    public IEdmModel GetEdmModel()
    {
        EdmModel model = new EdmModel();
        // ......
        return model;
    }

    The Edm Model built by this way is called un-typed (or typeless, week type) Edm model. Owing that there is no corresponding CLR classes.

    Non-convention model builder

    To build an Edm model using non-convention model builder is to create an IEdmModel object by directly call fluent APIs of ODataModelBuilder. The developer should take all responsibility to add all Edm types, operations, associations, etc into the data model one by one. The basic code structure of this way is shown as:

    public static IEdmModel GetEdmModel()
    {
        var builder = new ODataModelBuilder();
        // ......
        return builder.GetEdmModel();
    }

    Convention model builder

    To build an Edm model using convention model builder is to create an IEdmModel object by a set of conventions. Such conventions are pre-defined rules in Web API OData to help model builder to identify Edm types, keys, association etc automatically, and build them into the final Edm model. ODataConventionModelBuilder wrappers these conventions and apply them to the Edm model when building. The basic code structure of this way is shown as:

    public static IEdmModel GetEdmModel()
    {
        var builder = new ODataConventionModelBuilder();
        // ......
        return builder.GetEdmModel();
    }

    Basically, it’s recommended to use convention model builder to build Edm model for its simplicity and convenience. However, if user wants to make more control on the model building, or he doesn’t have the corresponding CLR classes, the non-convention model builder and the explicitly method are also very useful.

    2.1.2 Edm Model Sample

    We’ll build an Edm model using the above three methods in the following sections respectively. Each section is designed to walk you through every required aspect to build such Edm model. First of all, let’s have a brief view about the Edm model we will build. This is a Customer-Order business model, in which three entity types, two complex types and one enum type are included. Here’s the detail information about each types:

    Customer is served as an entity type with three properties.

    public class Customer
    {
        public int CustomerId { get; set; } // structural property, primitive type, key
        public Address Location { get; set; } // structural property, complex type
        public IList<Order> Orders { get; set; } // navigation property, entity type
    }

    VipCustomer is an entity type derived from Customer. It includes one more property:

    public class VipCustomer : Customer
    {
        public Color FavoriteColor { get; set; } // structural property, primitive type
    }

    Order is another entity type with two properties.

    public class Order
    {
        public int OrderId { get; set; } // structural property, primitive type, key
        public Guid Token { get; set; } // structural property, primitive type
    }

    Address & SubAddress are served as complex types, while SubAddress is derived from Address.

    public class Address
    {
        public string Country { get; set; }
        public string City { get; set; }
    }
    
    public class SubAddress : Address
    {
        public string Street { get; set; }
    }

    Color is served as an Enum Type.

    public enum Color
    {
        Red,
        Blue,
        Green
    }

    Here’s the class heritance:

  • 2.2 Build Edm Model Explicitly

    As mentioned in previous section, to build Edm model explicitly is to create an IEdmModel object directly using ODatalib API. The Edm model built by this method is called type-less model, or week type model, or just un-typed model.

    Let’s see how to build the Customer-Order business model.

    Enum Type

    We can use EdmEnumType to define an Enum type Color as:

    EdmEnumType color = new EdmEnumType("WebApiDocNS", "Color");
    color.AddMember(new EdmEnumMember(color, "Red", new EdmIntegerConstant(0)));
    color.AddMember(new EdmEnumMember(color, "Blue", new EdmIntegerConstant(1)));
    color.AddMember(new EdmEnumMember(color, "Green", new EdmIntegerConstant(2)));
    model.AddElement(color);

    It will generate the below metadata document:

    <EnumType Name="Color">
       <Member Name="Red" Value="0" />
       <Member Name="Blue" Value="1" />
       <Member Name="Green" Value="2" />
    </EnumType>

    Complex Type

    Basic Complex Type

    We can use EdmComplexType to define a complex type Address as:

    EdmComplexType address = new EdmComplexType("WebApiDocNS", "Address");
    address.AddStructuralProperty("Country", EdmPrimitiveTypeKind.String);
    address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String);
    model.AddElement(address);

    It will generate the below metadata document:

    <ComplexType Name="Address">
      <Property Name="Country" Type="Edm.String" />
      <Property Name="City" Type="Edm.String" />
    </ComplexType>

    Derived Complex type

    We can set the base type in construct to define a derived complex type SubAddress as:

    EdmComplexType subAddress = new EdmComplexType("WebApiDocNS", "SubAddress", address);
    subAddress.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String);
    model.AddElement(subAddress);

    It will generate the below metadata document:

    <ComplexType Name="SubAddress" BaseType="WebApiDocNS.Address">
      <Property Name="Street" Type="Edm.String" />
    </ComplexType>

    Other Complex Types

    We can call the following construct to set a complex type whether it is abstract or open.

    public EdmComplexType(string namespaceName, string name, IEdmComplexType baseType, bool isAbstract, bool isOpen);

    For example:

    EdmComplexType address = new EdmComplexType("WebApiDocNS", "Address", baseType: null, isAbstract: true, isOpen: true);
    model.AddElement(address);

    It will generate the below metadata document:

    <ComplexType Name="Address" Abstract="true" OpenType="true" />

    Entity Type

    Basic Entity Type

    We can use EdmEntityType to define two entity types Customer & Order as:

    EdmEntityType customer = new EdmEntityType("WebApiDocNS", "Customer");
    customer.AddKeys(customer.AddStructuralProperty("CustomerId", EdmPrimitiveTypeKind.Int32));
    customer.AddStructuralProperty("Location", new EdmComplexTypeReference(address, isNullable: true));
    model.AddElement(customer);
    
    EdmEntityType order = new EdmEntityType("WebApiDocNS", "Order");
    order.AddKeys(order.AddStructuralProperty("OrderId", EdmPrimitiveTypeKind.Int32));
    order.AddStructuralProperty("Token", EdmPrimitiveTypeKind.Guid);
    model.AddElement(order);

    It will generate the below metadata document:

    <EntityType Name="Customer">
      <Key>
        <PropertyRef Name="CustomerId" />
      </Key>
      <Property Name="CustomerId" Type="Edm.Int32" />
      <Property Name="Location" Type="WebApiDocNS.Address" />
    </EntityType>
    <EntityType Name="Order">
      <Key>
        <PropertyRef Name="OrderId" />
      </Key>
      <Property Name="OrderId" Type="Edm.Int32" />
      <Property Name="Token" Type="Edm.Guid" />
    </EntityType>

    Derived Entity type

    We can set the base type in construct to define a derived entity type VipCustomer as:

    EdmEntityType vipCustomer = new EdmEntityType("WebApiDocNS", "VipCustomer", customer);
    vipCustomer.AddStructuralProperty("FavoriteColor", new EdmEnumTypeReference(color, isNullable: false));
    model.AddElement(vipCustomer);

    It will generate the below metadata document:

    <EntityType Name="VipCustomer" BaseType="WebApiDocNS.Customer">
        <Property Name="FavoriteColor" Type="WebApiDocNS.Color" Nullable="false" />
    </EntityType>

    Other Entity Types

    We can call the following construct to set an entity type whether it is abstract or open.

    public EdmEntityType(string namespaceName, string name, IEdmEntityType baseType, bool isAbstract, bool isOpen);

    For example:

    EdmEntityType customer = new EdmEntityType("WebApiDocNS", "Customer", baseType: null, isAbstract: true, isOpen: true);
    model.AddElement(customer);

    It will generate the below metadata document:

    <EntityType Name="Customer" Abstract="true" OpenType="true" />

    Default Entity Container

    Each model MUST define at most one entity container, in which entity sets, singletons and operation imports are defined. For example:

    EdmEntityContainer container = new EdmEntityContainer("WebApiDocNS", "Container");
    EdmEntitySet customers = container.AddEntitySet("Customers", customer);
    EdmEntitySet orders = container.AddEntitySet("Orders", order);
    model.AddElement(container);

    It will generate the below metadata document:

    <EntityContainer Name="Container">
       <EntitySet Name="Customers" EntityType="WebApiDocNS.Customer" />
       <EntitySet Name="Orders" EntityType="WebApiDocNS.Order" />
    </EntityContainer>

    Singleton

    We can also add singleton into entity container. For example:

    EdmSingleton mary = container.AddSingleton("Mary", customer);

    It will generate the below metadata document:

    <Singleton Name="Mary" Type="WebApiDocNS.Customer" />

    Now, we can add navigation property to Customer. For example:

    EdmNavigationProperty ordersNavProp = customer.AddUnidirectionalNavigation(
        new EdmNavigationPropertyInfo
        {
            Name = "Orders",
            TargetMultiplicity = EdmMultiplicity.Many,
            Target = order
        });
    customers.AddNavigationTarget(ordersNavProp, orders);

    It will generate the below metadata document:

    First, it will add a new item in the entity type as:

    <EntityType Name="Customer">
        ...
        <NavigationProperty Name="Orders" Type="Collection(WebApiDocNS.Order)" />
    </EntityType>

    Second, it will add a new item in the entity container for Customers entity set as:

    <EntitySet Name="Customers" EntityType="WebApiDocNS.Customer">
      <NavigationPropertyBinding Path="Orders" Target="Orders" />
    </EntitySet>

    Function

    Let’s define two functions. One is bound, the other is unbound as:

    IEdmTypeReference stringType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false);
    IEdmTypeReference intType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false);
    // Bound
    EdmFunction getFirstName = new EdmFunction("WebApiDocNS", "GetFirstName", stringType, isBound: true, entitySetPathExpression: null, isComposable: false);
    getFirstName.AddParameter("entity", new EdmEntityTypeReference(customer, false));
    model.AddElement(getFirstName);
    
    // Unbound
    EdmFunction getNumber = new EdmFunction("WebApiDocNS", "GetOrderCount", intType, isBound: false, entitySetPathExpression: null, isComposable: false);
    model.AddElement(getNumber);

    It will generate the below metadata document:

    <Function Name="GetFirstName" IsBound="true">
       <Parameter Name="entity" Type="WebApiDocNS.Customer" Nullable="false" />
       <ReturnType Type="Edm.String" Nullable="false" />
    </Function>
    <Function Name="GetOrderCount">
       <ReturnType Type="Edm.Int32" Nullable="false" />
    </Function>

    Action

    Let’s define two actions. One is bound, the other is unbound as:

    // Bound
    EdmAction calculate = new EdmAction("WebApiDocNS", "CalculateOrderPrice", returnType: null, isBound: true, entitySetPathExpression: null);
    calculate.AddParameter("entity", new EdmEntityTypeReference(customer, false));
    model.AddElement(calculate);
    
    // Unbound
    EdmAction change = new EdmAction("WebApiDocNS", "ChangeCustomerById", returnType: null, isBound: false, entitySetPathExpression: null);
    change.AddParameter("Id", intType);
    model.AddElement(change);

    It will generate the below metadata document:

    <Action Name="CalculateOrderPrice" IsBound="true">
      <Parameter Name="entity" Type="WebApiDocNS.Customer" Nullable="false" />
    </Action>
    <Action Name="ChangeCustomerById">
      <Parameter Name="Id" Type="Edm.Int32" Nullable="false" />
    </Action>

    Function Import

    Unbound function can be called through function import. The following codes are used to build a function import:

    container.AddFunctionImport("GetOrderCount", getNumber);

    It will generate the below metadata document:

    <FunctionImport Name="GetOrderCount" Function="WebApiDocNS.GetOrderCount" />

    Action Import

    Unbound actioin can be called through action import. The following codes are used to build an action import:

    container.AddActionImport("ChangeCustomerById", change);

    It will generate the below metadata document:

    <ActionImport Name="ChangeCustomerById" Action="WebApiDocNS.ChangeCustomerById" />

    Summary

    Let’s put all codes together:

    public static IEdmModel GetEdmModel()
    {
        EdmModel model = new EdmModel();
    
        // Complex Type
        EdmComplexType address = new EdmComplexType("WebApiDocNS", "Address");
        address.AddStructuralProperty("Country", EdmPrimitiveTypeKind.String);
        address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String);
        model.AddElement(address);
    
        EdmComplexType subAddress = new EdmComplexType("WebApiDocNS", "SubAddress", address);
        subAddress.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String);
        model.AddElement(subAddress);
    
        // Enum type
        EdmEnumType color = new EdmEnumType("WebApiDocNS", "Color");
        color.AddMember(new EdmEnumMember(color, "Red", new EdmIntegerConstant(0)));
        color.AddMember(new EdmEnumMember(color, "Blue", new EdmIntegerConstant(1)));
        color.AddMember(new EdmEnumMember(color, "Green", new EdmIntegerConstant(2)));
        model.AddElement(color);
    
        // Entity type
        EdmEntityType customer = new EdmEntityType("WebApiDocNS", "Customer");
        customer.AddKeys(customer.AddStructuralProperty("CustomerId", EdmPrimitiveTypeKind.Int32));
        customer.AddStructuralProperty("Location", new EdmComplexTypeReference(address, isNullable: true));
        model.AddElement(customer);
    
        EdmEntityType vipCustomer = new EdmEntityType("WebApiDocNS", "VipCustomer", customer);
        vipCustomer.AddStructuralProperty("FavoriteColor", new EdmEnumTypeReference(color, isNullable: false));
        model.AddElement(vipCustomer);
    
        EdmEntityType order = new EdmEntityType("WebApiDocNS", "Order");
        order.AddKeys(order.AddStructuralProperty("OrderId", EdmPrimitiveTypeKind.Int32));
        order.AddStructuralProperty("Token", EdmPrimitiveTypeKind.Guid);
        model.AddElement(order);
    
        EdmEntityContainer container = new EdmEntityContainer("WebApiDocNS", "Container");
        EdmEntitySet customers = container.AddEntitySet("Customers", customer);
        EdmEntitySet orders = container.AddEntitySet("Orders", order);
        model.AddElement(container);
    
        // EdmSingleton mary = container.AddSingleton("Mary", customer);
    
        // navigation properties
        EdmNavigationProperty ordersNavProp = customer.AddUnidirectionalNavigation(
            new EdmNavigationPropertyInfo
            {
                Name = "Orders",
                TargetMultiplicity = EdmMultiplicity.Many,
                Target = order
            });
        customers.AddNavigationTarget(ordersNavProp, orders);
    	
        // function
        IEdmTypeReference stringType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false);
        IEdmTypeReference intType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false);
    
        EdmFunction getFirstName = new EdmFunction("WebApiDocNS", "GetFirstName", stringType, isBound: true, entitySetPathExpression: null, isComposable: false);
        getFirstName.AddParameter("entity", new EdmEntityTypeReference(customer, false));
        model.AddElement(getFirstName);
    
        EdmFunction getNumber = new EdmFunction("WebApiDocNS", "GetOrderCount", intType, isBound: false, entitySetPathExpression: null, isComposable: false);
        model.AddElement(getNumber);
        container.AddFunctionImport("GetOrderCount", getNumber);
    
        // action
        EdmAction calculate = new EdmAction("WebApiDocNS", "CalculateOrderPrice", returnType: null, isBound: true, entitySetPathExpression: null);
        calculate.AddParameter("entity", new EdmEntityTypeReference(customer, false));
        model.AddElement(calculate);
    
        EdmAction change = new EdmAction("WebApiDocNS", "ChangeCustomerById", returnType: null, isBound: false, entitySetPathExpression: null);
        change.AddParameter("Id", intType);
        model.AddElement(change);
        container.AddActionImport("ChangeCustomerById", change);
    
        return model;
    }

    And the final XML will be:

    <?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="WebApiDocNS" xmlns="http://docs.oasis-open.org/odata/ns/edm">
          <ComplexType Name="Address">
            <Property Name="Country" Type="Edm.String" />
            <Property Name="City" Type="Edm.String" />
          </ComplexType>
          <ComplexType Name="SubAddress" BaseType="WebApiDocNS.Address">
            <Property Name="Street" Type="Edm.String" />
          </ComplexType>
          <EnumType Name="Color">
            <Member Name="Red" Value="0" />
            <Member Name="Blue" Value="1" />
            <Member Name="Green" Value="2" />
          </EnumType>
          <EntityType Name="Customer">
            <Key>
              <PropertyRef Name="CustomerId" />
            </Key>
            <Property Name="CustomerId" Type="Edm.Int32" />
            <Property Name="Location" Type="WebApiDocNS.Address" />
            <NavigationProperty Name="Orders" Type="Collection(WebApiDocNS.Order)" />
          </EntityType>
          <EntityType Name="VipCustomer" BaseType="WebApiDocNS.Customer">
            <Property Name="FavoriteColor" Type="WebApiDocNS.Color" Nullable="false" />
          </EntityType>
          <EntityType Name="Order">
            <Key>
              <PropertyRef Name="OrderId" />
            </Key>
            <Property Name="OrderId" Type="Edm.Int32" />
            <Property Name="Token" Type="Edm.Guid" />
          </EntityType>
          <Function Name="GetFirstName" IsBound="true">
            <Parameter Name="entity" Type="WebApiDocNS.Customer" Nullable="false" />
            <ReturnType Type="Edm.String" Nullable="false" />
          </Function>
          <Function Name="GetOrderCount">
            <ReturnType Type="Edm.Int32" Nullable="false" />
          </Function>
          <Action Name="CalculateOrderPrice" IsBound="true">
            <Parameter Name="entity" Type="WebApiDocNS.Customer" Nullable="false" />
          </Action>
          <Action Name="ChangeCustomerById">
            <Parameter Name="Id" Type="Edm.Int32" Nullable="false" />
          </Action>
          <EntityContainer Name="Container">
            <EntitySet Name="Customers" EntityType="WebApiDocNS.Customer">
              <NavigationPropertyBinding Path="Orders" Target="Orders" />
            </EntitySet>
            <EntitySet Name="Orders" EntityType="WebApiDocNS.Order" />
            <FunctionImport Name="GetOrderCount" Function="WebApiDocNS.GetOrderCount" />
            <ActionImport Name="ChangeCustomerById" Action="WebApiDocNS.ChangeCustomerById" />
          </EntityContainer>
        </Schema>
      </edmx:DataServices>
    </edmx:Edmx>
  • 2.3 Non-convention model builder

    To build an Edm model using non-convention model builder is to create an IEdmModel object by directly call fluent APIs of ODataModelBuilder. The developer should take all responsibility to add all Edm types, operations, associations, etc into the data model one by one. Let’s see how to build the Ccustomer-Order* business model by ODataModelBuilder.

    CLR Models

    Non-convention model builder is based on CLR classes to build the Edm Model. The Customer-Order business CLR classes are present in abstract section.

    Enum Type

    The following codes are used to add an Enum type Color:

    var color = builder.EnumType<Color>();
    color.Member(Color.Red);
    color.Member(Color.Blue);
    color.Member(Color.Green);

    It will generate the below metadata document:

    <EnumType Name="Color">
       <Member Name="Red" Value="0" />
       <Member Name="Blue" Value="1" />
       <Member Name="Green" Value="2" />
    </EnumType>

    Complex Type

    Basic Complex Type

    The following codes are used to add a complex type Address:

    var address = builder.ComplexType<Address>();
    address.Property(a => a.Country);
    address.Property(a => a.City);

    It will generate the below metadata document:

    <ComplexType Name="Address">
      <Property Name="Country" Type="Edm.String" />
      <Property Name="City" Type="Edm.String" />
    </ComplexType>

    Derived Complex type

    The following codes are used to add a derived complex type SubAddress:

    var subAddress = builder.ComplexType<SubAddress>().DerivesFrom<Address>();
    subAddress.Property(s => s.Street);

    It will generate the below metadata document:

    <ComplexType Name="SubAddress" BaseType="WebApiDocNS.Address">
      <Property Name="Street" Type="Edm.String" />
    </ComplexType>

    Abstract Complex type

    The following codes are used to add an abstract complex type:

    builder.ComplexType<Address>().Abstract();
    ......

    It will generate the below metadata document:

    <ComplexType Name="Address" Abstract="true">
      ......
    </ComplexType>

    Open Complex type

    In order to build an open complex type, you should change the CLR class by adding an IDictionary<string, object> property, the property name can be any name. For example:

    public class Address
    {
        public string Country { get; set; }
        public string City { get; set; }
        public IDictionary<string, object> Dynamics { get; set; }
    }

    Then you can build the open complex type by call HasDynamicProperties():

    var address = builder.ComplexType<Address>();
    address.Property(a => a.Country);
    address.Property(a => a.City);
    address.HasDynamicProperties(a => a.Dynamics);

    It will generate the below metadata document:

    <ComplexType Name="Address" OpenType="true">
      <Property Name="Country" Type="Edm.String" />
      <Property Name="City" Type="Edm.String" />
    </ComplexType>

    You can find that the complex type Address only has two properties, while it has OpenType="true" attribute.

    Entity Type

    Basic Entity Type

    The following codes are used to add two entity types Customer & Order:

    var customer = builder.EntityType<Customer>();
    customer.HasKey(c => c.CustomerId);
    customer.ComplexProperty(c => c.Location);
    customer.HasMany(c => c.Orders);
    
    var order = builder.EntityType<Order>();
    order.HasKey(o => o.OrderId);
    order.Property(o => o.Token);

    It will generate the below metadata document:

    <EntityType Name="Customer">
        <Key>
            <PropertyRef Name="CustomerId" />
        </Key>
        <Property Name="CustomerId" Type="Edm.Int32" Nullable="false" />
        <Property Name="Location" Type="WebApiDocNS.Address" Nullable="false" />
        <NavigationProperty Name="Orders" Type="Collection(WebApiDocNS.Order)" />
    </EntityType>
    <EntityType Name="Order">
        <Key>
            <PropertyRef Name="OrderId" />
        </Key>
        <Property Name="OrderId" Type="Edm.Int32" Nullable="false" />
        <Property Name="Token" Type="Edm.Guid" Nullable="false" />
    </EntityType>

    Abstract Open type

    The following codes are used to add an abstract entity type:

    builder.EntityType<Customer>().Abstract();
    ......

    It will generate the below metadata document:

    <EntityType Name="Customer" Abstract="true">
      ......
    </EntityType>

    Open Entity type

    In order to build an open entity type, you should change the CLR class by adding an IDictionary<string, object> property, while the property name can be any name. For example:

    public class Customer
    {
        public int CustomerId { get; set; }
        public Address Location { get; set; }
        public IList<Order> Orders { get; set; }
        public IDictionary<string, object> Dynamics { get; set; }
    }

    Then you can build the open entity type as:

    var customer = builder.EntityType<Customer>();
    customer.HasKey(c => c.CustomerId);
    customer.ComplexProperty(c => c.Location);
    customer.HasMany(c => c.Orders);
    customer.HasDynamicProperties(c => c.Dynamics);

    It will generate the below metadata document:

    <EntityType Name="Customer" OpenType="true">
        <Key>
            <PropertyRef Name="CustomerId" />
        </Key>
        <Property Name="CustomerId" Type="Edm.Int32" Nullable="false" />
        <Property Name="Location" Type="WebApiDocNS.Address" Nullable="false" />
        <NavigationProperty Name="Orders" Type="Collection(WebApiDocNS.Order)" />
    </EntityType>

    You can find that the entity type Customer only has three properties, while it has OpenType="true" attribute.

    Entity Container

    Non-convention model builder will build the default entity container automatically. However, you should build your own entity sets as:

    builder.EntitySet<Customer>("Customers");
    builder.EntitySet<Order>("Orders");

    It will generate the below metadata document:

    <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
        <EntityContainer Name="Container">
            <EntitySet Name="Customers" EntityType="WebApiDocNS.Customer">
              <NavigationPropertyBinding Path="Orders" Target="Orders" />
            </EntitySet>
            <EntitySet Name="Orders" EntityType="WebApiDocNS.Order" />
        </EntityContainer>
    </Schema>

    Besides, you can call Singleton<T>() to add singleton into entity container.

    Function

    It’s very simple to build function (bound & unbound) in Web API OData. The following codes define two functions. The first is bind to Customer, the second is unbound.

    var function = customer.Function("BoundFunction").Returns<string>();
    function.Parameter<int>("value");
    function.Parameter<Address>("address");
    
    function = builder.Function("UnBoundFunction").Returns<int>();
    function.Parameter<Color>("color");
    function.EntityParameter<Order>("order");

    It will generate the below metadata document:

    <Function Name="BoundFunction" IsBound="true">
       <Parameter Name="bindingParameter" Type="WebApiDocNS.Customer" />
       <Parameter Name="value" Type="Edm.Int32" Nullable="false" />
       <Parameter Name="address" Type="WebApiDocNS.Address" />
       <ReturnType Type="Edm.String" Unicode="false" />
    </Function>
    <Function Name="UnBoundFunction">
       <Parameter Name="color" Type="WebApiDocNS.Color" Nullable="false" />
       <Parameter Name="order" Type="WebApiDocNS.Order" />
       <ReturnType Type="Edm.Int32" Nullable="false" />
    </Function>

    Besides, Web API OData will automatically add function imports for all unbound functions. So, the metadata document should has:

    <FunctionImport Name="UnBoundFunction" Function="Default.UnBoundFunction" IncludeInServiceDocument="true" />

    Action

    Same as function, it’s also very simple to build action (bound & unbound) in Web API OData. The following codes define two actions. The first is bind to collection of Customer, the second is unbound.

    var action = customer.Collection.Action("BoundAction");
    action.Parameter<int>("value");
    action.CollectionParameter<Address>("addresses");
    
    action = builder.Action("UnBoundAction").Returns<int>();
    action.Parameter<Color>("color");
    action.CollectionEntityParameter<Order>("orders");

    It will generate the below metadata document:

    <Action Name="BoundAction" IsBound="true">
        <Parameter Name="bindingParameter" Type="Collection(WebApiDocNS.Customer)" />
        <Parameter Name="value" Type="Edm.Int32" Nullable="false" />
        <Parameter Name="addresses" Type="Collection(WebApiDocNS.Address)" />
    </Action>
    <Action Name="UnBoundAction">
        <Parameter Name="color" Type="WebApiDocNS.Color" Nullable="false" />
        <Parameter Name="orders" Type="Collection(WebApiDocNS.Order)" />
        <ReturnType Type="Edm.Int32" Nullable="false" />
    </Action>

    Same as function, Web API OData will automatically add action imports for all unbound actions. So, the metadata document should has:

     <ActionImport Name="UnBoundAction" Action="Default.UnBoundAction" />

    Summary

    Let’s put all codes together:

    public static IEdmModel GetEdmModel()
    {
        var builder = new ODataModelBuilder();
    
        // enum type
        var color = builder.EnumType<Color>();
        color.Member(Color.Red);
        color.Member(Color.Blue);
        color.Member(Color.Green);
    
        // complex type
        // var address = builder.ComplexType<Address>().Abstract();
        var address = builder.ComplexType<Address>();
        address.Property(a => a.Country);
        address.Property(a => a.City);
        // address.HasDynamicProperties(a => a.Dynamics);
    
        var subAddress = builder.ComplexType<SubAddress>().DerivesFrom<Address>();
        subAddress.Property(s => s.Street);
    
        // entity type
        // var customer = builder.EntityType<Customer>().Abstract();
        var customer = builder.EntityType<Customer>();
        customer.HasKey(c => c.CustomerId);
        customer.ComplexProperty(c => c.Location);
        customer.HasMany(c => c.Orders);
        // customer.HasDynamicProperties(c => c.Dynamics);
    
        var order = builder.EntityType<Order>();
        order.HasKey(o => o.OrderId);
        order.Property(o => o.Token);
    
        // entity set
        builder.EntitySet<Customer>("Customers");
        builder.EntitySet<Order>("Orders");
    
        // function
        var function = customer.Function("BoundFunction").Returns<string>();
        function.Parameter<int>("value");
        function.Parameter<Address>("address");
    
        function = builder.Function("UnBoundFunction").Returns<int>();
        function.Parameter<Color>("color");
        function.EntityParameter<Order>("order");
    
        // action
        var action = customer.Collection.Action("BoundAction");
        action.Parameter<int>("value");
        action.CollectionParameter<Address>("addresses");
    
        action = builder.Action("UnBoundAction").Returns<int>();
        action.Parameter<Color>("color");
        action.CollectionEntityParameter<Order>("orders");
    
        return builder.GetEdmModel();
    }

    And the final XML will be:

    <?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="WebApiDocNS" xmlns="http://docs.oasis-open.org/odata/ns/edm">
          <ComplexType Name="Address">
            <Property Name="Country" Type="Edm.String" />
            <Property Name="City" Type="Edm.String" />
          </ComplexType>
          <ComplexType Name="SubAddress" BaseType="WebApiDocNS.Address">
            <Property Name="Street" Type="Edm.String" />
          </ComplexType>
          <EntityType Name="Customer" OpenType="true">
            <Key>
              <PropertyRef Name="CustomerId" />
            </Key>
            <Property Name="CustomerId" Type="Edm.Int32" Nullable="false" />
            <Property Name="Location" Type="WebApiDocNS.Address" Nullable="false" />
            <NavigationProperty Name="Orders" Type="Collection(WebApiDocNS.Order)" />
          </EntityType>
          <EntityType Name="Order">
            <Key>
              <PropertyRef Name="OrderId" />
            </Key>
            <Property Name="OrderId" Type="Edm.Int32" Nullable="false" />
            <Property Name="Token" Type="Edm.Guid" Nullable="false" />
          </EntityType>
          <EnumType Name="Color">
            <Member Name="Red" Value="0" />
            <Member Name="Blue" Value="1" />
            <Member Name="Green" Value="2" />
          </EnumType>
        </Schema>
        <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
          <Function Name="BoundFunction" IsBound="true">
            <Parameter Name="bindingParameter" Type="WebApiDocNS.Customer" />
            <Parameter Name="value" Type="Edm.Int32" Nullable="false" />
            <Parameter Name="address" Type="WebApiDocNS.Address" />
            <ReturnType Type="Edm.String" Unicode="false" />
          </Function>
          <Function Name="UnBoundFunction">
            <Parameter Name="color" Type="WebApiDocNS.Color" Nullable="false" />
            <Parameter Name="order" Type="WebApiDocNS.Order" />
            <ReturnType Type="Edm.Int32" Nullable="false" />
          </Function>
          <Action Name="BoundAction" IsBound="true">
            <Parameter Name="bindingParameter" Type="Collection(WebApiDocNS.Customer)" />
            <Parameter Name="value" Type="Edm.Int32" Nullable="false" />
            <Parameter Name="addresses" Type="Collection(WebApiDocNS.Address)" />
          </Action>
          <Action Name="UnBoundAction">
            <Parameter Name="color" Type="WebApiDocNS.Color" Nullable="false" />
            <Parameter Name="orders" Type="Collection(WebApiDocNS.Order)" />
            <ReturnType Type="Edm.Int32" Nullable="false" />
          </Action>
          <EntityContainer Name="Container">
            <EntitySet Name="Customers" EntityType="WebApiDocNS.Customer">
              <NavigationPropertyBinding Path="Orders" Target="Orders" />
            </EntitySet>
            <EntitySet Name="Orders" EntityType="WebApiDocNS.Order" />
            <FunctionImport Name="UnBoundFunction" Function="Default.UnBoundFunction" IncludeInServiceDocument="true" />
            <ActionImport Name="UnBoundAction" Action="Default.UnBoundAction" />
          </EntityContainer>
        </Schema>
      </edmx:DataServices>
    </edmx:Edmx>
  • 2.4 Convention model builder

    In the previous two sections, we walk you through the required aspects to build an Edm model by directly using ODatalib or leveraging ODataModelBuilder fluent API in WebApi OData.

    Obvious, there are many codes you should add to develop a simple Customer-Order business model. However, Web API OData also provides a simple method by using ODataConventionModelBuilder to do the same thing. It’s called convention model builder and can extremely reduce your workload.

    Convention model builder uses a set of pre-defined rules (called conventions) to help model builder identify Edm types, keys, associations, relationships, etc automatically, and build them into the final Edm data model.

    In this section, we will go through all conventions used in convention model builder. First, let’s see how to build the Edm model using ODataConventionModelBuilder.

    CLR Models

    We also use the Customer-Order business model presented in abstract section.

    Build the Edm Model

    The following codes can add all related entity types, complex types, enum type and the corresponding entity sets into the Edm model:

    public static IEdmModel GetConventionModel()
    {
       var builder = new ODataConventionModelBuilder();
       builder.EntitySet<Customer>("Customers");
       builder.EntitySet<Order>("Orders");
    
       return builder.GetEdmModel();
    }

    It will generate the below metadata document:

    <?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="WebApiDocNS" xmlns="http://docs.oasis-open.org/odata/ns/edm">
          <EntityType Name="Customer" OpenType="true">
            <Key>
              <PropertyRef Name="CustomerId" />
            </Key>
            <Property Name="CustomerId" Type="Edm.Int32" Nullable="false" />
            <Property Name="Location" Type="WebApiDocNS.Address" />
            <NavigationProperty Name="Orders" Type="Collection(WebApiDocNS.Order)" />
          </EntityType>
          <EntityType Name="Order">
            <Key>
              <PropertyRef Name="OrderId" />
            </Key>
            <Property Name="OrderId" Type="Edm.Int32" Nullable="false" />
            <Property Name="Token" Type="Edm.Guid" Nullable="false" />
          </EntityType>
          <ComplexType Name="Address" OpenType="true">
            <Property Name="Country" Type="Edm.String" />
            <Property Name="City" Type="Edm.String" />
          </ComplexType>
          <ComplexType Name="SubAddress" BaseType="WebApiDocNS.Address" OpenType="true">
            <Property Name="Street" Type="Edm.String" />
          </ComplexType>
          <EntityType Name="VipCustomer" BaseType="WebApiDocNS.Customer" OpenType="true">
            <Property Name="FavoriteColor" Type="WebApiDocNS.Color" Nullable="false" />
          </EntityType>
          <EnumType Name="Color">
            <Member Name="Red" Value="0" />
            <Member Name="Blue" Value="1" />
            <Member Name="Green" Value="2" />
          </EnumType>
        </Schema>
        <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
          <EntityContainer Name="Container">
            <EntitySet Name="Customers" EntityType="WebApiDocNS.Customer">
              <NavigationPropertyBinding Path="Orders" Target="Orders" />
            </EntitySet>
            <EntitySet Name="Orders" EntityType="WebApiDocNS.Order" />
          </EntityContainer>
        </Schema>
      </edmx:DataServices>
    </edmx:Edmx>

    Note: We omit the function/action building because it’s same as non-convention model builder.

    Conventions

    Wow, how the convention model builder do that! Actually, convention model builder uses a set of pre-defined rules (called conventions) to achieve this. If you open the source code for ODataConventionModelBuilder, You can find the following codes at the beginning of the ODataConventionModelBuilder class:

    private static readonly List<IConvention> _conventions = new List<IConvention>
    {
    	// type and property conventions (ordering is important here).
    	new AbstractTypeDiscoveryConvention(),
    	new DataContractAttributeEdmTypeConvention(),
    	new NotMappedAttributeConvention(), // NotMappedAttributeConvention has to run before EntityKeyConvention
    	new DataMemberAttributeEdmPropertyConvention(),
    	new RequiredAttributeEdmPropertyConvention(),
    	new ConcurrencyCheckAttributeEdmPropertyConvention(),
    	new TimestampAttributeEdmPropertyConvention(),
    	new KeyAttributeEdmPropertyConvention(), // KeyAttributeEdmPropertyConvention has to run before EntityKeyConvention
    	new EntityKeyConvention(),
    	new ComplexTypeAttributeConvention(), // This has to run after Key conventions, basically overrules them if there is a ComplexTypeAttribute
    	new IgnoreDataMemberAttributeEdmPropertyConvention(),
    	new NotFilterableAttributeEdmPropertyConvention(),
    	new NonFilterableAttributeEdmPropertyConvention(),
    	new NotSortableAttributeEdmPropertyConvention(),
    	new UnsortableAttributeEdmPropertyConvention(),
    	new NotNavigableAttributeEdmPropertyConvention(),
    	new NotExpandableAttributeEdmPropertyConvention(),
    	new NotCountableAttributeEdmPropertyConvention(),
    
    	// INavigationSourceConvention's
    	new SelfLinksGenerationConvention(),
    	new NavigationLinksGenerationConvention(),
    	new AssociationSetDiscoveryConvention(),
    
    	// IEdmFunctionImportConventions's
    	new ActionLinkGenerationConvention(),
    	new FunctionLinkGenerationConvention(),
    };

    Where lists the conventions wrapped in convention model builder. However, in ODataConventionModelBuilder, there are some conventions which can’t be clearly listed. Let’s walk you through these conventions one by one with some relevant attributes & annotations to illustrate the convention model builder.

    Type Inheritance Identify Convention

    Rule: Only derived types can be walked.

    For example:

    public class Base
    {
        public int Id { get; set; }
    }
    
    public class Derived : Base
    {
    }

    By using convention builder:

    public static IEdmModel GetEdmModel()
    {
        ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntityType<Base>();
        return builder.GetEdmModel()
    }

    It will generate the below entity type in the resulted EDM document:

    <EntityType Name="Base">
      <Key>
        <PropertyRef Name="Id" />
      </Key>
      <Property Name="Id" Type="Edm.Int32" Nullable="false" />
    </EntityType>
    <EntityType Name="Derived" BaseType="WebApiDocNS.Base" />

    If you change the model builder as:

    public static IEdmModel GetEdmModel()
    {
        ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntityType<Derived>();
        return builder.GetEdmModel()
    }

    It will generate the below entity type in the resulted EDM document:

    <EntityType Name="Derived">
      <Key>
        <PropertyRef Name="Id" />
      </Key>
      <Property Name="Id" Type="Edm.Int32" Nullable="false" />
    </EntityType>

    There is no Base entity type existed.

    Abstract type convention

    Rule: The Edm type will be marked as abstract if the class is abstract.

    public abstract class Base
    {
        public int Id { get; set; }
    }

    The result is :

    <EntityType Name="Base" Abstract="true">
       <Key>
         <PropertyRef Name="Id" />
       </Key>
       <Property Name="Id" Type="Edm.Int32" Nullable="false" />
    </EntityType>

    Entity key convention

    Rule: If one and only one property’s name is ‘Id’ or ‘<entity class name>Id’ (case insensitive), it becomes entity key property.

    public abstract class Base
    {
        public int BaseId { get; set; }
    }

    The result is:

    <EntityType Name="Base" Abstract="true">
      <Key>
        <PropertyRef Name="BaseId" />
      </Key>
      <Property Name="BaseId" Type="Edm.Int32" Nullable="false" />
    </EntityType>

    Key attribute

    Rule: The [KeyAttribute] specifies key property, it forces a property without ‘id’ to be Key property. It’ll suppress the entity key convention.

    public class Trip
    {
        [Key]
        public int TripNum { get; set; }	
        public int Id { get; set; }
    }

    The result is:

    <EntityType Name="Trip">
      <Key>
        <PropertyRef Name="TripNum" />
      </Key>
      <Property Name="TripNum" Type="Edm.Int32" Nullable="false" />
      <Property Name="Id" Type="Edm.Int32" Nullable="false" />
    </EntityType>

    ComplexType Attribute Convention

    Rule-1: Create a class without any ‘id’ or ‘id’ or [`KeyAttribute`] property, like

    public class City
    {
        public string Name { get; set; }
        public string CountryRegion { get; set; }
        public string Region { get; set; }
    }

    Rule-2: Add [ComplexType] attribute to a model class: it will remove ‘id’ or ‘id’ or [Key] properties, the model class will have no entity key, thus becomes a complex type.

    [ComplexType]
    public class PairItem
    {
        public int Id { get; set; }
        public string Value { get; set; }
    }

    Then, the above two types wil be built as Complex Type.

    DataContract & DataMember

    Rule: If using DataContract or DataMember, only property with [DataMember] attribute will be added into Edm model.

    [DataContract]
    public class Trip
    {
        [DataMember]
        [Key]
        public int TripNum { get; set; }
        public Guid? ShareId { get; set; }  // will be eliminated
        [DataMember]
        public string Name { get; set; }
    }

    The resulted EDM document is:

    <EntityType Name="Trip">
      <Key>
    	<PropertyRef Name="TripNum" />
      </Key>
      <Property Name="TripNum" Type="Edm.Int32" Nullable="false" />
      <Property Name="Name" Type="Edm.String" />
    </EntityType>

    You can also change name-space and property name in EDM document. For example, if the above DataContract attribute is added with NameSpace:

    [DataContract(Namespace="My.NewNameSpace")]
    public class Trip
    { 
       ...
    }

    The result will become:

    <Schema Namespace="My.NewNameSpace">
      <EntityType Name="Trip">
    	<Key>
    	  <PropertyRef Name="TripNum"/>
    	</Key>
    	<Property Name="TripNum" Type="Edm.Int32" Nullable="false"/>
    	<Property Name="Name" Type="Edm.String"/>
      </EntityType>
    </Schema>

    NotMapped Attribute Convention

    Rule: [NotMapped] deselects the property to be serialized or deserialized, so to some extent, it can be seen as the converse of DataContract & DataMember.

    For example, the above if Trip class is changed to the below, it generates exactly the same Trip Entity in EDM document, that is, no ‘SharedId’ property.

    [DataContract]
    public class Trip
    {
        [DataMember]
        [Key]
        public int TripNum { get; set; }
        [NotMapped]
        public Guid? ShareId { get; set; }
        [DataMember]
        public string Name { get; set; }
    }	

    The result is:

    <EntityType Name="Trip">
      <Key>
    	<PropertyRef Name="TripNum"/>
      </Key>
      <Property Name="TripNum" Type="Edm.Int32" Nullable="false"/>
      <Property Name="Name" Type="Edm.String"/>
    </EntityType>

    Required Attribute Convention

    Rule: The property with [Required] attribute will be non-nullable.

    public class Trip
    {
        [Key]
        public int TripNum { get; set; }
        [NotMapped]
        public Guid? ShareId { get; set; }
        [Required]
        public string Name { get; set; }
    }

    Then the result has Nullable=”false” for Name property:

    <EntityType Name="Trip">
      <Key>
    	<PropertyRef Name="TripNum"/>
      </Key>
      <Property Name="TripNum" Type="Edm.Int32" Nullable="false"/>
      <Property Name="Name" Type="Edm.String" Nullable="false"/>
    </EntityType>

    ConcurrencyCheck Attribute Convention

    Rule: It can mark one or more properties for doing optimistic concurrency check on entity updates.

    public class Trip
    {
        [Key]
        public int TripNum { get; set; }
        public int Id { get; set; }
        [ConcurrencyCheck]
        public string UpdateVersion { get; set; }
    }

    The expected result should be like the below:

    <EntityType Name="Trip">
        <Key>
            <PropertyRef Name="TripNum" />
        </Key>
        <Property Name="TripNum" Type="Edm.Int32" Nullable="false" />
        <Property Name="Id" Type="Edm.Int32" Nullable="false" />
        <Property Name="UpdateVersion" Type="Edm.String" ConcurrencyMode="Fixed" />
    </EntityType>

    Timestamp Attribute Convention

    Rule: It’s same as [ConcurrencyCheck].

    public class Trip
    {
        [Key]
        public int TripNum { get; set; }
        public int Id { get; set; }
        [Timestamp]
        public string UpdateVersion { get; set; }
    }

    The expected result should be like the below:

    <EntityType Name="Trip">
        <Key>
            <PropertyRef Name="TripNum" />
        </Key>
        <Property Name="TripNum" Type="Edm.Int32" Nullable="false" />
        <Property Name="Id" Type="Edm.Int32" Nullable="false" />
        <Property Name="UpdateVersion" Type="Edm.String" ConcurrencyMode="Fixed" />
    </EntityType>

    IgnoreDataMember Attribute Convention

    Rule: It has the same effect as [NotMapped] attribute. It is able to revert the [DataMember] attribute on the property when the model class doesn’t have [DataContract] attribute.

    NonFilterable & NotFilterable Attribute Convention

    Rule: Property marked with [NonFilterable] or [NotFilterable] will not support $filter query option.

    public class QueryLimitCustomer
    {
        public int Id { get; set; }
    
        [NonFilterable]
        [NotFilterable]
        public string Title { get; set; }
    }

    Then, if you issue an query option as: ~/odata/Customers?$filter=Title eq 'abc'

    You will get the following exception:

    The query specified in the URI is not valid. The property 'Title' cannot be used in the $filter query option.

    NotSortable & Unsortable Attribute Convention

    Rule: Property marked with [NotSortable] or [Unsortable] will not support $orderby query option.

    public class QueryLimitCustomer
    {
        public int Id { get; set; }
    
        [NotSortable]
        [Unsortable]	
        public string Title { get; set; }
    }

    Then, if you issue an query option as: `~/odata/Customers?$orderby=Title

    You will get the following exception:

    The query specified in the URI is not valid. The property 'Title' cannot be used in the $orderby query option.

    NotNavigable Attribute Convention

    Rule: Property marked with [NotNavigable] will not support $select query option.

    public class QueryLimitCustomer
    {
        public int Id { get; set; }
    
        [NotNavigable]	
        public Address address { get; set; }
    }

    Then, if you issue an query option as: `~/odata/Customers?$select=address

    You will get the following exception:

    The query specified in the URI is not valid. The property 'address' cannot be used for navigation."

    NotExpandable Attribute Convention

    Rule: Property marked with [NotExpandable] will not support $expand query option.

    public class QueryLimitCustomer
    {
        public int Id { get; set; }
    
        [NotExpandable]	
        public ICollection<QueryLimitOrder> Orders { get; set; }
    }

    Then, if you issue an query option as: `~/odata/Customers?$expand=Orders

    You will get the following exception:

    The query specified in the URI is not valid. The property 'Orders' cannot be used in the $expand query option.

    NotCountable Attribute Convention

    Rule: Property marked with [NotCountable] will not support $count query option.

    public class QueryLimitCustomer
    {
        public int Id { get; set; }
    
        [NotCountable]	
        public ICollection<Address> Addresses { get; set; }
    }

    Then, if you issue an query option as: `~/odata/Customers(1)/Addresses?$count=true

    You will get the following exception:

    The query specified in the URI is not valid. The property 'Addresses' cannot be used for $count.

    ForeignKey Attribute Convention

    Rule: Property marked with [ForeignKey] will be used to build referential constraint.

    public class ForeignCustomer
    {
        public int ForeignCustomerId { get; set; }
        public int OtherCustomerKey { get; set; }
        public IList<ForeignOrder> Orders { get; set; }
    }
    
    public class ForeignOrder
    {
        public int ForeignOrderId { get; set; }
        public int CustomerId { get; set; }
    
        [ForeignKey("CustomerId")]
        public ForeignCustomer Customer { get; set; }
    }

    You will get the following result:

    <EntityType Name="ForeignOrder">
    <Key>
      <PropertyRef Name="ForeignOrderId" />
    </Key>
    <Property Name="ForeignOrderId" Type="Edm.Int32" Nullable="false" />
    <Property Name="CustomerId" Type="Edm.Int32" />
    <NavigationProperty Name="Customer" Type="WebApiDocNS.ForeignCustomer">
      <ReferentialConstraint Property="CustomerId" ReferencedProperty="ForeignCustomerId" />
    </NavigationProperty>
    </EntityType>

    [ForeignKey] can also put on dependent property. for example:

    public class ForeignOrder
    {
        public int ForeignOrderId { get; set; }
    	
        [ForeignKey("Customer")]
        public int CustomerId { get; set; }
        public ForeignCustomer Customer { get; set; }
    }

    It’ll get the same result.

    ActionOnDelete Attribute Convention

    Rule: Property marked with [ActionOnDelete] will be used to build referential constraint action on delete.

    public class ForeignCustomer
    {
        public int ForeignCustomerId { get; set; }
        public int OtherCustomerKey { get; set; }
        public IList<ForeignOrder> Orders { get; set; }
    }
    
    public class ForeignOrder
    {
        public int ForeignOrderId { get; set; }
        public int CustomerId { get; set; }
    
        [ForeignKey("CustomerId")]
        [ActionOnDelete(EdmOnDeleteAction.Cascade)]
        public ForeignCustomer Customer { get; set; }
    }

    You will get the following result:

    <EntityType Name="ForeignOrder">
    <Key>
      <PropertyRef Name="ForeignOrderId" />
    </Key>
    <Property Name="ForeignOrderId" Type="Edm.Int32" Nullable="false" />
    <Property Name="CustomerId" Type="Edm.Int32" />
    <NavigationProperty Name="Customer" Type="WebApiDocNS.ForeignCustomer">
      <OnDelete Action="Cascade" />
      <ReferentialConstraint Property="CustomerId" ReferencedProperty="ForeignCustomerId" />
    </NavigationProperty>
    </EntityType>

    ForeignKeyDiscovery Convention

    Rule: A convention used to discover foreign key properties if there is no any foreign key configured on the navigation property. The basic rule to discover the foreign key is: with the same property type and follow up the naming convention. The naming convention is: 1. The “Principal class name + principal key name” equals the dependent property name For example: Customer (Id) <–> Order (CustomerId) 2. or the “Principal key name” equals the dependent property name. For example: Customer (CustomerId) <–> Order (CustomerId)

      
    public class PrincipalEntity
    {
        public string Id { get; set; }
    }
    
    public class DependentEntity
    {
        public int Id { get; set; }
    
        public string PrincipalEntityId { get; set; }
        public PrincipalEntity Principal { get; set; }
    }		  

    You will get the following result:

    <EntityType Name="PrincipalEntity">
    <Key>
      <PropertyRef Name="Id" />
    </Key>
    <Property Name="Id" Type="Edm.String" Nullable="false" />
    </EntityType>
    <EntityType Name="DependentEntity">
    <Key>
      <PropertyRef Name="Id" />
    </Key>
    <Property Name="Id" Type="Edm.Int32" Nullable="false" />
    <Property Name="PrincipalEntityId" Type="Edm.String" />
    <NavigationProperty Name="Principal" Type="WebApiDocNS.PrincipalEntity">
    
      <ReferentialConstraint Property="PrincipalEntityId" ReferencedProperty="Id" />
    </NavigationProperty>
    </EntityType>
  • 2.5 Summary

    In this chapter, we describe the three methods used to build the Edm model in Web API OData and walk you through every required aspect to build a simple Customer-Order Edm model. It’s recommended to use convention model builder to build Edm model for its simplicity and convenience. However, if user wants to make more control on the model building, or he doesn’t have the corresponding CLR classes, the non-convention model builder and the explicitly method are also very useful.

3. ROUTING

  • 3.1 Introduction Routing

    In Web API, Routing is how it matches a request URI to an action in a controller. The Routing of Web API OData is derived from Web API Routing and do more extensions. In Web API OData, an OData controller (not API controller) is severed as the request handler to handle HTTP requests, while the public methods (called action methods) in the controller are invoked to execute the business logic. So, when the client issues a request to OData service, the Web API OData framework will map the request to an action in the OData controller. Such mapping is based on pre-registered Routes in global configuration.

    Register the Web API OData Routes

    In Web API, developer can use the following codes to register a Web API route into routing table:

    configuration.routes.MapHttpRoute(
        name: "myRoute",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = ... }
    );

    While, Web API OData re-uses the Web API routing table to register the Web OData Routes. However it provides its own extension method called MapODataServiceRoute to register the OData route. MapODataServiceRoute has many versions, here’s the basic usage:

    HttpConfiguration configuration = new HttpConfiguration();
    configuration.MapODataServiceRoute(routeName:"myRoute", routePrefix:"odata", model: GetEdmModel()));

    With these codes, we register an OData route named “myRoute”, uses “odata” as prefix and by calling GetEdmModel() to set up the Edm model.

    After registering the Web OData routes, we define an OData route template in the routing table. The route template has the following syntax:

    ~/odata/~

    Now, the Web API OData framework can handle the HTTP request. It tries to match the request Uri against one of the route templates in the routing table. Basically, the following URIs match the odata route:

    ~/odata/Customers
    ~/odata/Customers(1)
    ~/odata/Customers/Default.MyFunction()

    Where, Customers is the entity set names.

    However, the following URI does not match the odata route, because it doesn’t match “odata” prefix segment:

    ~/myodata/Customers(1)

    Routing Convention

    Once the odata route is found, Web API OData will parse the request Uri to get the path segments. Web API OData first uses the ODatalib to parse the request Uri to get the ODL path segments, then convert the ODL path segments to Web API OData path segments. Once the Uri Parse is finished, Web API OData will try to find the corresponding OData controller and action. The process to find controller and action are the main part of Routing Convention. Basically, there are two parts of Routing Convention:

    1. Convention Routing

      It is also called built-in routing conventions. It uses a set of pre-defined rules to find controller and action.

    2. Attribute Routing

      It uses two Attributes to find controller and action. One is ODataRoutePrefixAttribute, the other is ODataRouteAttribute.

  • 3.2 Built-in routing conventions

    When Web API gets an OData request, it maps the request to a controller name and an action name. The mapping is based on the HTTP method and the URI. For example, GET /odata/Products(1) maps to ProductsController.GetProduct.

    This article describe the built-in OData routing conventions. These conventions are designed specifically for OData endpoints, and they replace the default Web API routing system. (The replacement happens when you call MapODataRoute.)

    Built-in Routing Conventions

    Before describe the OData routing conventions in Web API, it’s helpful to understand OData URIs. An OData URI consists of:

    • The service root
    • The odata path
    • Query options

    For example: http://example.com/odata/Products(1)/Supplier?$top=2

    • The service root : http://example.com/odata
    • The odata path : Products(1)/Supplier
    • Query options : ?$top=2

    For OData routing, the important part is the OData path. The OData path is divided into segments, each segments are seperated with ‘/’.

    [For example], Products(1)/Supplier has three segments:

    • Products refers to an entity set named “Products”.
    • 1 is an entity key, selecting a single entity from the set.
    • Supplier is a navigation property that selects a related entity.

    So this path picks out the supplier of product 1.

    OData path segments do not always correspond to URI segments. For example, “1” is considered a key path segment.

    Controller Names. The controller name is always derived from the entity set at the root of the OData path. For example, if the OData path is Products(1)/Supplier, Web API looks for a controller named ProductsController.

    So, the controller convention is: [entityset name] + “Controller”, derived from ODataController

    Action Names. Action names are derived from the path segments plus the entity data model (EDM), as listed in the following tables. In some cases, you have two choices for the action name. For example, “Get” or “GetProducts”.

    Querying Entities

    Creating, Updating, and Deleting Entities

    Operation on Navigation Property

    Querying, Creating and Deleting Links

    Properties

    Request Example URI Action Name Example Action
    GET /entityset(key)/property /Products(1)/Name GetPropertyFromEntityType or GetProperty GetNameFromProduct
    GET /entityset(key)/cast/property /Products(1)/Models.Book/Author GetPropertyFromEntityType or GetProperty GetTitleFromBook

    Actions

    Action only supports the POST request method, and the parameters are sent using the request body. In controller, each action is using an ODataActionParameters to accept the parameters’ value:

    Functions

    Functions only supports the GET request method.

    Method Signatures

    Here are some rules for the method signatures:

    • If the path contains a key, the action should have a parameter named key.
    • If the path contains a key into a navigation property, the action should have a parameter named relatedKey.
    • POST and PUT requests take a parameter of the entity type.
    • PATCH requests take a parameter of type Delta, where T is the entity type.

    For reference, here is an example that shows method signatures for most built-in OData routing convention.

    public class ProductsController : ODataController
    {
        // GET /odata/Products
        public IQueryable<Product> Get()
    
        // GET /odata/Products(1)
        public Product Get(int key)
    
        // GET /odata/Products(1)/ODataRouting.Models.Book
        public Book GetBook(int key)
    
        // POST /odata/Products 
        public HttpResponseMessage Post(Product item)
    
        // PUT /odata/Products(1)
        public HttpResponseMessage Put(int key, Product item)
    
        // PATCH /odata/Products(1)
        public HttpResponseMessage Patch(int key, Delta<Product> item)
    
        // DELETE /odata/Products(1)
        public HttpResponseMessage Delete(int key)
    
        // PUT /odata/Products(1)/ODataRouting.Models.Book
        public HttpResponseMessage PutBook(int key, Book item)
    
        // PATCH /odata/Products(1)/ODataRouting.Models.Book
        public HttpResponseMessage PatchBook(int key, Delta<Book> item)
    
        // DELETE /odata/Products(1)/ODataRouting.Models.Book
        public HttpResponseMessage DeleteBook(int key)
    
        // GET /odata/Products(1)/Supplier
        public Supplier GetSupplierFromProduct(int key)
    
        // GET /odata/Products(1)/ODataRouting.Models.Book/Author
        public Author GetAuthorFromBook(int key)
    
        // POST /odata/Products(1)/Supplier/$ref
        public HttpResponseMessage CreateLink(int key, 
            string navigationProperty, [FromBody] Uri link)
    
        // DELETE /odata/Products(1)/Supplier/$ref
        public HttpResponseMessage DeleteLink(int key, 
            string navigationProperty, [FromBody] Uri link)
    
        // DELETE /odata/Products(1)/Parts(1)/$ref
        public HttpResponseMessage DeleteLink(int key, string relatedKey, string navigationProperty)
    
        // GET odata/Products(1)/Name
        // GET odata/Products(1)/Name/$value
        public HttpResponseMessage GetNameFromProduct(int key)
    
        // GET /odata/Products(1)/ODataRouting.Models.Book/Title
        // GET /odata/Products(1)/ODataRouting.Models.Book/Title/$value
        public HttpResponseMessage GetTitleFromBook(int key)
    }
        

    Update form Routing Conventions in OData V3.0

  • 3.3 Attribute Routing

    Same as Web API, Web API OData supports a new type of routing called attribute routing. It uses two Attributes to find controller and action. One is ODataPrefixAttribute, the other is ODataRouteAttribute.

    You can use attribute routing to define more complex routes and put more control over the routing. Most important, it can extend the coverage of convention routing. For example, you can easily use attribute routing to route the following Uri:

    ~/odata/Customers(1)/Orders/Price

    In Web API OData, attribute routing is combined with convention routing by default.

    Enabling Attribute Routing

    ODataRoutingConventions provides two methods to register routing conventions:

    public static IList<IODataRoutingConvention> CreateDefaultWithAttributeRouting(HttpConfiguration configuration, IEdmModel model)
    
    public static IList<IODataRoutingConvention> CreateDefault()

    As the name implies, the first one creates a mutable list of the default OData routing conventions with attribute routing enabled, while the second one only includes convention routing.

    In fact, when you call the basic MapODataServiceRoute, it enables the attribute routing by default as:

    public static ODataRoute MapODataServiceRoute(this HttpConfiguration configuration, string routeName, string routePrefix, IEdmModel model, ODataBatchHandler batchHandler)
    {
        return MapODataServiceRoute(configuration, routeName, routePrefix, model, new DefaultODataPathHandler(),
            ODataRoutingConventions.CreateDefaultWithAttributeRouting(configuration, model), batchHandler);
    }

    However, you can call other version of MapODataServiceRoute to custom your own routing conventions. For example:

    public static ODataRoute MapODataServiceRoute(this HttpConfiguration configuration, string routeName, string routePrefix, IEdmModel model, IODataPathHandler pathHandler, IEnumerable<IODataRoutingConvention> routingConventions)

    ODataRouteAttribute

    ODataRouteAttribute is an attribute that can, and only can be placed on an action of an OData controller to specify the OData URLs that the action handles.

    Here is an example of an action defined using an ODataRouteAttribute:

    public class MyController : ODataController
    {
        [HttpGet]
        [ODataRoute("Customers({id})/Address/City")]
        public string GetCityOfACustomer([FromODataUri]int id)
        {
            ......
        }
    }

    With this attribute, Web API OData tries to match the request Uri with Customers({id})/Address/City routing template to GetCityOfACustomer() function in MyController. For example, the following request Uri will invoke GetCityOfACustomer:

    ~/odata/Customers(1)/Address/City
    ~/odata/Customers(2)/Address/City
    ~/odata/Customers(301)/Address/City

    For the above request Uri, id in the function will have 1, 2 and 301 value.

    However, for the following request Uri, it can’t match to `GetCityOfACustomer()’:

    ~/odata/Customers
    ~/odata/Customers(1)/Address

    Web API OData supports to put multiple ODataRouteAttribute on the same OData action. For example,

    public class MyController : ODataController
    {
        [HttpGet]
        [ODataRoute("Customers({id})/Address/City")]
        [ODataRoute("Products({id})/Address/City")]
        public string GetCityOfACustomer([FromODataUri]int id)
        {
            ......
        }
    }

    ODataRoutePrefixAttribute

    ODataRoutePrefixAttribute is an attribute that can, and only can be placed on an OData controller to specify the prefix that will be used for all actions of that controller.

    ODataRoutePrefixAttribute is used to reduce the routing template in ODataRouteAttribute if all routing template in the controller start with the same prefix. For example:

    public class MyController : ODataController
    {
        [ODataRoute("Customers({id})/Address")]
        public IHttpActionResult GetAddress(int id)
        {
            ......
        }
    
        [ODataRoute("Customers({id})/Address/City")]
        public IHttpActionResult GetCity(int id)
        {
            ......
        }
    
        [ODataRoute("Customers({id})/Order")]
        public IHttpActionResult GetOrder(int id)
        {
            ......
        }
    }

    Then you can use ODataRoutePrefixAttribute attribute on the controller to set a common prefix.

    [ODataRoutePrefix("Customers({id})")]
    public class MyController : ODataController
    {
        [ODataRoute("Address")]
        public IHttpActionResult GetAddress(int id)
        {
            ......
        }
    
        [ODataRoute("Address/City")]
        public IHttpActionResult GetCity(int id)
        {
            ......
        }
    
        [ODataRoute("/Order")]
        public IHttpActionResult GetOrder(int id)
        {
            ......
        }
    }

    Now, Web API OData supports to put multiple ODataRoutePrefixAttribute on the same OData controller. For example,

    [ODataRoutePrefix("Customers({key})")]  
    [ODataRoutePrefix("VipCustomer")]  
    public class ODataControllerWithMultiplePrefixes : ODataController  
    {
        ......  
    }

    Route template

    The route template is the route combined with ODataRoutePrefixAttribute and ODataRouteAttribute. So, for the following example:

    [ODataRoutePrefix("Customers")]  
    public class MyController : ODataController  
    {
        [ODataRoute("({id})/Address")]
        public IHttpActionResult GetAddress(int id)
        {
            ......
        }
    }

    The GetAddress matches to Customers({id})/Address route template. It’s called key template because there’s a template {id}. So far in Web API OData, it supports two kind of templates:

    1. key template, for example:
    [ODataRoute("({id})/Address")]
    [ODataRoute("Clients({clientId})/MyOrders({orderId})/OrderLines")]
    1. function parameter template, for example:
    [ODataRoute("Customers({id})/NS.MyFunction(city={city})")]
    [ODataRoute("Customers/Default.BoundFunction(SimpleEnum={p1})")]

    Web API OData team also works to add the third template, that is the dynamic property template. It’s planed to ship in next release.

    You can refer to this blog for attribute routing in Web API 2.

  • 3.4 Custom routing convention

    It’s easy to custom your own routing convention to override the default Web API OData routing convention. Let’s see how to target it.

    Property access routing convention

    From built-in routing convention section, we know that users should add many actions for every property access.

    For example, if the client issues the following property access request Uris:

    ~/odata/Customers(1)/Orders
    ~/odata/Customers(1)/Address
    ~/odata/Customers(1)/Name
    ...

    Service should have the following actions in CustomersController to handle:

    public class CustomersController : ODataController
    {
        public string GetOrders([FromODataUri]int key)
        {
            ......
        }
    	
        public string GetAddress([FromODataUri]int key)
        {
            ......
        }
    	
        public string GetName([FromODataUri]int key)
        {
            ......
        }
    }

    If Customer has hundreds of properties, users should add hundres of similar functions in CustomersController. It’s boring and we can create our own routing convention to override it.

    Custom routing convention

    We can create our own routing convention class by implementing the IODataRoutingConvention. However, if you don’t want to change the behaviour to find the controller, the new added routing convention class can derive from `NavigationSourceRoutingConvention’.

    Let’s build a sample property access routing convention class derived from NavigationSourceRoutingConvention.

    public class CustomPropertyRoutingConvention : NavigationSourceRoutingConvention
    {
      private const string ActionName = "GetProperty";
    
      public override string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, ILookup<string, HttpActionDescriptor> actionMap)
      {
        if (odataPath == null || controllerContext == null || actionMap == null)
        {
           return null;
        }
    
        if (odataPath.PathTemplate == "~/entityset/key/property" ||
            odataPath.PathTemplate == "~/entityset/key/cast/property" ||
    	odataPath.PathTemplate == "~/singleton/property" ||
    	odataPath.PathTemplate == "~/singleton/cast/property")
        {
          var segment = odataPath.Segments[odataPath.Segments.Count - 1] as PropertyAccessPathSegment;
    
          if (segment != null)
          {
     	  string actionName = FindMatchingAction(actionMap, ActionName);
    
    	  if (actionName != null)
    	  {
    	    if (odataPath.PathTemplate.StartsWith("~/entityset/key", StringComparison.Ordinal))
    	    {
    	      KeyValuePathSegment keyValueSegment = odataPath.Segments[1] as KeyValuePathSegment;
    	      controllerContext.RouteData.Values[ODataRouteConstants.Key] = keyValueSegment.Value;
    	    }
    
    	    controllerContext.RouteData.Values["propertyName"] = segment.PropertyName;
    
    	    return actionName;
    	  }
    	}
         }
    
         return null;
       }
    
       public static string FindMatchingAction(ILookup<string, HttpActionDescriptor> actionMap, params string[] targetActionNames)
       {
         foreach (string targetActionName in targetActionNames)
         {
           if (actionMap.Contains(targetActionName))
           {
       	  return targetActionName;
           }
         }
    
         return null;
       }
    }

    Where, we routes the following path templates to a certain action named GetProperty.

    ~/entityset/key/property
    ~/entityset/key/cast/property
    ~/singleton/property
    ~/singleton/cast/property

    Enable customized routing convention

    The following sample codes are used to enable the customized routing convention:

    HttpConfiguration configuration = ......
    IEdmModel model = GetEdmModel();
    IList<IODataRoutingConvention> conventions = ODataRoutingConventions.CreateDefaultWithAttributeRouting(configuration, model);
    conventions.Insert(0, new CustomPropertyRoutingConvention());
    configuration.MapODataServiceRoute("odata", "odata", model, new DefaultODataPathHandler(), conventions);

    Where, we insert our own routing convention at the starting position to override the default Web API OData property access routing convention.

    Add actions

    In the CustomersController, only one method named GetProperty should be added.

    public class CustomersController : ODataController
    {
    	[HttpGet]
    	public IHttpActionResult GetProperty(int key, string propertyName)
    	{
    		Customer customer = _customers.FirstOrDefault(c => c.CustomerId == key);
    		if (customer == null)
    		{
    			return NotFound();
    		}
    
    		PropertyInfo info = typeof(Customer).GetProperty(propertyName);
    
    		object value = info.GetValue(customer);
    
    		return Ok(value, value.GetType());
    	}
    	
    	private IHttpActionResult Ok(object content, Type type)
    	{
    		var resultType = typeof(OkNegotiatedContentResult<>).MakeGenericType(type);
    		return Activator.CreateInstance(resultType, content, this) as IHttpActionResult;
    	}
    }

    Samples

    Let’s have some request Uri samples to test:

    a)

    GET http://localhost/odata/Customers(2)/Name

    The result is:

    {
      "@odata.context":"http://localhost/odata/$metadata#Customers(2)/Name","value": "Mike"
    }

    b)

    GET http://localhost/odata/Customers(2)/Location

    The result is:

    {
      "@odata.context":"http://localhost/odata/$metadata#Customers(2)/Salary","value ":2000.0
    }

    c)

    GET http://localhost/odata/Customers(2)/Location

    The result is:

    {
      "@odata.context":"http://localhost/odata/$metadata#Customers(2)/Location","Country":"The United States","City":"Redmond"
    }

4. ODATA FEATURES

  • 4.1 DateTime support

    This sample will introduce how to support DateTime type in Web API OData V4.

    Build DateTime Type

    OData V4 doesn’t include DateTime as primitive type. Web API OData V4 uses DateTimeOffset to represent the DateTime. For example, if user defines a model as:

    public class Customer
    {
        public int Id { get; set; }
    
        public DateTime Birthday { get; set; }
    }

    The metadata document for Customer entity type will be:

    <EntityType Name="Customer">
        <Key>
            <PropertyRef Name="Id" />
        </Key>
        <Property Name="Id" Type="Edm.Int32" Nullable="false" />
        <Property Name="Birthday" Type="Edm.DateTimeOffset" Nullable="false" />
    </EntityType>

    Time Zone Configuration

    By Default, converting between DateTimeOffset and DateTime will lose the Time Zone information. Therefore, Web API provides a API to config the Time Zone information on server side. For example:

    HttpConfiguration configuration = ...
    TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); // -8:00
    configuration.SetTimeZoneInfo(timeZoneInfo);

    $filter DateTime

    Since Web API OData 5.6, it supports to filter on DateTime type. For example:

    GET ~/Customers?$filter=Birthday lt cast(2015-04-01T04:11:31%2B08:00,Edm.DateTimeOffset)
    GET ~/Customers?$filter=year(Birthday) eq 2010

    $orderby DateTime

    Since Web API OData 5.6, it supports to orderby on DateTime type. For example:

    GET ~/Customers?$orderby=Birthday
    GET ~/Customers?$orderby=Birthday desc

    Thanks.

  • 4.2 Referential constraint

    The following sample codes can be used for Web API OData V3 & V4 with a little bit function name changing.

    Define Referential Constraint Using Attribute

    There is an attribute named “ForeignKeyAttribute” which can be place on:

    1.the foreign key property and specify the associated navigation property name, for example:

    public class Order
    {
        public int OrderId { get; set; }
    
        [ForeignKey("Customer")]
        public int MyCustomerId { get; set; }
    
        public Customer Customer { get; set; }
    }

    2.a navigation property and specify the associated foreign key name, for example:

    public class Order
    {
        public int OrderId { get; set; }
    
        public int CustId1 { get; set; }
        public string CustId2 { get; set; }
    
        [ForeignKey("CustId1,CustId2")]
        public Customer Customer { get; set; }
    }

    Where, Customer has two keys.

    Now, you can build the Edm Model by convention model builder as:

    public IEdmModel GetEdmModel()
    {            
        ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<Customer>("Customers");
        builder.EntitySet<Order>("Orders");
        return builder.GetEdmModel();
    }

    Define Referential Constraint Using Convention

    If user doesn’t add any referential constraint, Web API will try to help user to discovery the foreign key automatically. There are two conventions as follows: 1.With same property type and same type name plus key name. For example:

    public class Customer
    { 
       [Key]
       public string Id {get;set;}
       public IList<Order> Orders {get;set;}
    }
    public class Order
    {
        public int OrderId { get; set; }
        public string CustomerId {get;set;}
        public Customer Customer { get; set; }
    }

    Where, Customer type name “Customer” plus key name “Id” equals the property “CustomerId” in the Order.

    2.With same property type and same property name. For example:

    public class Customer
    { 
       [Key]
       public string CustomerId {get;set;}
       public IList<Order> Orders {get;set;}
    }
    
    public class Order
    {
        public int OrderId { get; set; }
        public string CustomerId {get;set;}
        public Customer Customer { get; set; }
    }

    Where, Property (key) “CustomerId” in the Customer equals the property “CustomerId” in the Order.

    Now, you can build the Edm Model using convention model builder same as above section.

    Define Referential Constraint Programmatically

    You can call the new added Public APIs (HasRequired, HasOptional) to define the referential constraint when defining a navigation property. For example:

    public class Customer
    {
        public int Id { get; set; }
           
        public IList<Order> Orders { get; set; }
    }
    
    public class Order
    {
        public int OrderId { get; set; }
     
        public int CustomerId { get; set; }         
    
        public Customer Customer { get; set; }
    }
    
    ODataModelBuilder builder = new ODataModelBuilder();
    builder.EntityType<Customer>().HasKey(c => c.Id).HasMany(c => c.Orders);
    builder.EntityType<Order>().HasKey(c => c.OrderId)
        .HasRequired(o => o.Customer, (o, c) => o.CustomerId == c.Id);
        .CascadeOnDelete();
        

    It also supports to define multiple referential constraints, for example:

    builder.EntityType<Order>()
        .HasKey(o => o.OrderId)
        .HasRequired(o => o.Customer, (o,c) => o.Key1 == c.Id1 && o.Key2 == c.Id2);
        

    Define Nullable Referential Constraint Using Convention

    Currently, it doesn’t suppport to define nullable referential constraint from attribute and convention method. However, you can do it by Programmatically by calling HasOptional() method:

    For example:

    public class Product
    {
        [Key]
        public int ProductId { get; set; }
    
        public int SupplierId { get; set; }
    
        public Supplier Supplier { get; set; }
    }
    
    public class Category
    {
        [Key]
        public int CategoryId { get; set; }
    }
    
    ODataModelBuilder builder = new ODataModelBuilder();
    builder.EntitySet<Product>("Product");
    var product = builder.EntityType<Product>().HasOptional(p => p.Supplier, (p, s) => p.SupplierId == s.SupplierId);
        

    Then you can get the following result:

    <EntityType Name="Product">
        <Key>
          <PropertyRef Name="ProductId" />
        </Key>
        <Property Name="SupplierId" Type="Edm.Int32" />
        <Property Name="ProductId" Type="Edm.Int32" Nullable="false" />
        <Property Name="CategoryId" Type="Edm.Int32" />
        <NavigationProperty Name="Supplier" Type="WebApiService.Supplier">
          <ReferentialConstraint Property="SupplierId" ReferencedProperty="SupplierId" />
        </NavigationProperty>
        <NavigationProperty Name="Category" Type="WebApiService.Category" />
    </EntityType>

    Where, CategoryId is nullable while navigation property Supplier is nullable too.

    Thanks.

  • 4.3 Nested $filter in $expand

    OData Web API v5.5 supports nested $filter in $expand, e.g.: .../Customers?$expand=Orders($filter=Id eq 10)

    POCO classes:

    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public IEnumerable<Order> Orders { get; set; }
    }
    
    public class Order
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    With Edm model built as follows:

    var builder = new ODataConventionModelBuilder(config);
    builder.EntitySet<Customer>("Customers");
    var model = builder.GetEdmModel();

    To Map route,

    • For Microsoft.AspNet.OData, e.g., in WebApiConfig.cs:
    config.MapODataServiceRoute("orest", "orest", model);
    • For Microsoft.AsnNetCore.OData, e.g., in Startup.Configure((IApplicationBuilder app, IHostingEnvironment env) method:
    app.UseMvc(routeBuilder => 
        {
            routeBuilder.Select().Expand().Filter().OrderBy().MaxTop(null).Count();
            routeBuilder.MapODataServiceRoute("orest", "orest", model);
        });

    Controller:

    public class CustomersController : ODataController
    {
        private Customer[] _customers =
        {
            new Customer
            {
                Id = 0,
                Name = "abc",
                Orders = new[]
                {
                    new Order { Id = 10, Name = "xyz" },
                    new Order { Id = 11, Name = "def" },
                }
            }
        };
    
        [EnableQuery]
        public IHttpActionResult Get()
        {
            return Ok(_customers.AsQueryable());
        }
    }

    Request: http://localhost:port_number/orest/Customers?$expand=Orders($filter=Id eq 10)

    Response:

    {
        "@odata.context": "http://localhost:52953/orest/$metadata#Customers",
        "value": [
            {
                "Id": 0,
                "Name": "abc",
                "Orders": [
                    {
                        "Id": 10,
                        "Name": "xyz"
                    }
                ]
            }
        ]
    }
  • 4.4 Edm.Date and Edm.TimeOfDay

    This sample introduces how to use the Edm.Date & Edm.TimeOfDay supported in Web API OData V5.5.

    Build Edm Model

    ODL V6.8 introduces two new primitive types. One is Edm.Date, the other is Edm.TimeOfDay. Besides, it also introduces two new struct types to represent the CLR types of Edm.Date and Edm.TimeOfDay. So, developers can use the new CLR struct types to define their CLR model. For example, if user defines a model as:

    using Microsoft.OData.Edm.Library;
    public class Customer
    {
        public int Id { get; set; }
    
        public DateTimeOffset Birthday { get; set; }
        
        public Date Publish { get; set; }
        
        public TimeOfDay CheckTime{ get; set;}
    }

    The metadata document for Customer entity type will be:

    <EntityType Name="Customer">
        <Key>
            <PropertyRef Name="Id" />
        </Key>
        <Property Name="Id" Type="Edm.Int32" Nullable="false" />
        <Property Name="Birthday" Type="Edm.DateTimeOffset" Nullable="false" />
        <Property Name="Publish" Type="Edm.Date" Nullable="false"/>
        <Property Name="CheckTime" Type="Edm.TimeOfDay" Nullable="false"/>
    </EntityType>

    Build-in Functions

    Along with the Edm.Date & Edm.TimeOfDay, new date and time related built-in functions are supported in Web API OData V5.5.

    Here’s the list:

    • Date
      • Edm.Int32 year(Edm.Date)
      • Edm.Int32 month(Edm.Date)
      • Edm.Int32 day(Edm.Date)
    • TimeOfDay
      • Edm.Int32 hour(Edm.TimeOfDay)
      • Edm.Int32 minute(Edm.TimeOfDay)
      • Edm.Int32 second(Edm.TimeOfDay)
      • Edm.Decimal fractionalseconds(Edm.TimeOfDay)
    • DateTimeOffset
      • Edm.Decimal fractionalseconds(Edm.DateTimeOffset)
      • Edm.Date date(Edm.DateTimeOffset)
      • Edm.TimeOfDay time(Edm.DateTimeOffset)

    Query examples

    Let’s show some query request examples:

    • Date
      • ~/odata/Customers?$filter=year(Publish) eq 2015
      • ~/odata/Customers?$filter=month(Publish) ne 11
      • ~/odata/Customers?$filter=day(Publish) lt 8
    • TimeOfDay
      • ~/odata/Customers?$filter=hour(CheckTime) eq 2
      • ~/odata/Customers?$filter=minute(CheckTime) ge 11
      • ~/odata/Customers?$filter=second(CheckTime) lt 18
      • ~/odata/Customers?$filter=fractionalseconds(CheckTime) eq 0.04
    • DateTimeOffset
      • ~/odata/Customers?$filter=fractionalseconds(Birthday) lt 0.04
      • ~/odata/Customers?$filter=date(Birthday) lt 2015-03-23
      • ~/odata/Customers?$filter=time(Birthday) eq 03:04:05.90100

    Thanks.

  • 4.5 Abstract entity types

    Since Web API OData V5.5-beta, it is allowed to:

    1. define abstract entity types without keys.
    2. define abstract type (entity & complex) without any properties.
    3. define derived entity types with their own keys.

    Let’s see some examples:

    Entity type example:

    The CLR model is shown as below:

    public abstract class Animal
    {
    }
    
    public class Dog : Animal
    {
      public int DogId { get; set; }
    }
    
    public class Pig : Animal
    {
      public int PigId { get; set; }
    }

    We can use the following codes to build Edm Model:

      var builder = new ODataConventionModelBuilder();
      builder.EntityType<Animal>();
      builder.EntitySet<Dog>("Dogs");
      builder.EntitySet<Pig>("Pigs");
      IEdmModel model = builder.GetEdmModel()

    Then, we can get the metadata document for Animal as:

    <EntityType Name="Animal" Abstract="true" />
    <EntityType Name="Dog" BaseType="NS.Animal">
        <Key>
            <PropertyRef Name="DogId" />
        </Key>
        <Property Name="DogId" Type="Edm.Int32" Nullable="false" />
    </EntityType>
    <EntityType Name="Pig" BaseType="NS.Animal">
        <Key>
            <PropertyRef Name="PigId" />
        </Key>
        <Property Name="PigId" Type="Edm.Int32" Nullable="false" />
    </EntityType>

    Note:

    1. Animal is an abstract entity type without any keys and any properties
    2. Dog & Pig are two sub entity types derived from Animal with own keys.

    However, it’s obvious that abstract entity type without keys can’t be used to define any navigation sources (entity set or singleton). So, if you try to:

    builder.EntitySet<Animal>("Animals");

    you will get the following exception:

    System.InvalidOperationException: The entity set or singlet on 'Animals' is based on type 'NS.Animal' that has no keys defined.

    Complex type example

    Let’s see a complex example. The CLR model is shown as below:

    public abstract class Graph
    { }
    
    public class Point : Graph
    {
      public int X { get; set; }
      public int Y { get; set; }
    }
    
    public class Line : Graph
    {
      public IList<Point> Vertexes { get; set; }
    }

    We can use the following codes to build Edm Model:

      var builder = new ODataConventionModelBuilder();
      builder.ComplexType<Graph>();
      IEdmModel model = builder.GetEdmModel()

    Then, we can get the metadata document for Graph as:

    <ComplexType Name="Graph" Abstract="true" />
    <ComplexType Name="Point" BaseType="NS.Graph">
        <Property Name="X" Type="Edm.Int32" Nullable="false" />
        <Property Name="Y" Type="Edm.Int32" Nullable="false" />
    </ComplexType>
    <ComplexType Name="Line" BaseType="NS.Graph">
        <Property Name="Vertexes" Type="Collection(NS.Point)" />
    </ComplexType>

    Where, Graph is an abstract complex type without any properties.

    Thanks.

  • 4.6 Function parameter support

    Since Web API OData V5.5-beta, it supports the following types as function parameter:

    1. Primitive
    2. Enum
    3. Complex
    4. Entity
    5. Entity Reference
    6. Collection of above

    Let’s see how to build and use the above types in function.

    CLR Model

    First of all, we create the following CLR classes as our model:

    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    
    public class SubCustomer : Customer
    {
        public double Price { get; set; }
    }
    
    public class Address
    {
        public string City { get; set; }
    }
    
    public class SubAddress : Address
    {
        public string Street { get; set; }
    }
    
    public enum Color
    {
        Red,
        Blue,
        Green
    }

    Build Edm Model

    Now, we can build the Edm Model as:

    private static IEdmModel GetEdmModel()
    {
        var builder = new ODataConventionModelBuilder();
        builder.EntitySet<Customer>("Customers");
        builder.ComplexType<Address>();
        builder.EnumType<Color>();
    
        BuildFunction(builder);
        return builder.GetEdmModel();
    }

    where, BuildFunction() is a helper function in which functions can be built.

    Primitive and Collection of Primitive parameter

    Configuration

    In BuildFunction(), we can configure a function with Primitive and collection of Primitive parameters:

    var function = builder.EntityType<Customer>().Collection.Function("PrimtiveFunction").Returns<string>();
    function.Parameter<int>("p1");
    function.Parameter<int?>("p2");
    function.CollectionParameter<int>("p3");

    Routing

    In the CustomersController, add the following method:

    [HttpGet]
    public string PrimtiveFunction(int p1, int? p2, [FromODataUri]IEnumerable<int> p3)
    {
       ...
    }

    Request Samples

    We can invoke the function as:

    ~/odata/Customers/Default.PrimitiveFunction(p1=1,p2=9,p3=[1,3,5,7,9])
    ~/odata/Customers/Default.PrimitiveFunction(p1=2,p2=null,p3=[1,3,5,7,9])
    ~/odata/Customers/Default.PrimitiveFunction(p1=3,p2=null,p3=@p)?@p=[2,4,6,8]

    Enum and Collection of Enum parameter

    Configuration

    In BuildFunction(), we can configure a function with Enum and collection of Enum parameters:

    var function = builder.EntityType<Customer>().Collection.Function("EnumFunction").Returns<string>();
    function.Parameter<Color>("e1");
    function.Parameter<Color?>("e2"); // nullable
    function.CollectionParameter<Color?>("e3"); // Collection of nullable

    Routing

    In the CustomersController, add the following method :

    [HttpGet]
    public string EnumFunction(Color e1, Color? e2, [FromODataUri]IEnumerable<Color?> e3)
    {
      ...
    }

    Request Samples

    We can invoke the Enum function as:

    ~/odata/Customers/Default.EnumFunction(e1=NS.Color'Red',e2=NS.Color'Green',e3=['Red', null, 'Blue'])
    ~/odata/Customers/Default.EnumFunction(e1=NS.Color'Blue',e2=null,e3=['Red', null, 'Blue'])
    ~/odata/Customers/Default.EnumFunction(e1=NS.Color'Blue',e2=null,e3=@p)?@p=['Red', null, 'Blue']

    Complex and Collection of Complex parameter

    Configuration

    In BuildFunction(), we can configure a function with Complex and collection of Complex parameters:

    var function = builder.EntityType<Customer>().Collection.Function("ComplexFunction").Returns<string>();
    function.Parameter<Address>("c1");
    function.CollectionParameter<Address>("c2");

    Routing

    In the CustomersController, add the following method :

    [HttpGet]
    public string ComplexFunction([FromODataUri]Address c1, [FromODataUri]IEnumerable<Address> c2)
    {
      ...
    }

    Request Samples

    We can invoke the complex function as:

    ~/odata/Customers/Default.ComplexFunction(c1=@x,c2=@y)?@x={\"@odata.type\":\"%23NS.Address\",\"City\":\"Redmond\"}&@y=[{\"@odata.type\":\"%23NS.Address\",\"City\":\"Redmond\"},{\"@odata.type\":\"%23NS.SubAddress\",\"City\":\"Shanghai\", \"Street\":\"Zi Xing Rd\"}]
    ~/odata/Customers/Default.ComplexFunction(c1={\"@odata.type\":\"%23NS.Address\",\"City\":\"Redmond\"},c2=[{\"@odata.type\":\"%23NS.Address\",\"City\":\"Redmond\"},{\"@odata.type\":\"%23NS.SubAddress\",\"City\":\"Shanghai\", \"Street\":\"Zi Xing Rd\"}])
    ~/odata/Customers/Default.ComplexFunction(c1=null,c2=@p)?@p=[null,{\"@odata.type\":\"%23NS.SubAddress\",\"City\":\"Shanghai\", \"Street\":\"Zi Xing Rd\"}]

    Entity and Collection of Entity parameter

    Configuration

    In BuildFunction(), we can configure a function with Entity and collection of Entity parameters:

    var function = builder.EntityType<Customer>().Collection.Function("EntityFunction").Returns<string>();
    function.EntityParameter<Customer>("a1");
    function.CollectionEntityParameter<Customer>("a2"); 

    It’s better to call EntityParameter<T> and CollectionEntityParameter<T> to define entity and collection of entity parameter.

    Routing

    In the CustomersController, add the following method :

    [HttpGet]
    public string EntityFunction([FromODataUri]Customer a1, [FromODataUri]IEnumerable<Customer> a2)
    {
      ...
    }

    Request Samples

    We can invoke the entity function as:

    ~/odata/Customers/Default.EntityFunction(a1=@x,a2=@y)?@x={\"@odata.type\":\"%23NS.Customer\",\"Id\":1,\"Name\":\"John\"}&@y={\"value\":[{\"@odata.type\":\"%23NS.Customer\",\"Id\":2, \"Name\":\"Mike\"},{\"@odata.type\":\"%23NS.SubCustomer\",\"Id\":3,\"Name\":\"Tony\", \"Price\":9.9}]}
    ~/odata/Customers/Default.EntityFunction(a1=@x,a2=@y)?@x=null&@y={\"value\":[]}

    However, only parameter alias is supported for entity.

    Entity Reference and collection of Entity Reference parameter

    In fact, we can’t build a function with entity reference as parameter. However, we can call the function with entity parameter using entity reference value. So, without any change for the EntityFunction, we can call as:

    ~/odata/Customers/Default.EntityFunction(a1=@x,a2=@y)?@x={\"@odata.id\":\"http://localhost/odata/Customers(2)\"}&@y={\"value\":[{\"@odata.id\":\"http://localhost/odata/Customers(2)\"},{\"@odata.id\":\"http://localhost/odata/Customers(3)\"}]}

    FromODataUri

    ‘[FromODataUri]’ is mandatory for complex, entity and all collection. However, it is optional for Primitive & Enum. But for string primitive type, the value will contain single quotes without ‘[FromODataUri]’.

    Thanks.

    For un-typed scenario, please refer to untyped page.

  • 4.7 Action parameter support

    Since Web API OData V5.5-beta, it supports the following types as action parameter:

    1. Primitive
    2. Enum
    3. Complex
    4. Entity
    5. Collection of above

    Let’s see how to build and use the above types in action.

    CLR Model

    Re-use the CLR models in function sample.

    Build Edm Model

    Same as build Edm Model in function sample, but change the helper function as BuildAction().

    Primitive and Collection of Primitive parameter

    Configuration

    In BuildAction(), we can configure an action with Primitive and collection of Primitive parameters:

    var action = builder.EntityType<Customer>().Collection.Action("PrimtiveAction");
    action.Parameter<int>("p1");
    action.Parameter<int?>("p2");
    action.CollectionParameter<int>("p3");

    Routing

    In the CustomersController, add the following method:

    [HttpPost]
    public IHttpActionResult PrimitiveAction(ODataActionParameters parameters)
    {
       ...
    }

    Request Samples

    We can invoke the action by issuing a Post on ~/odata/Customers/Default.PrimitiveAction with the following request body:

    {
      "p1":7,
      "p2":9,
      "p3":[1,3,5,7,9]
    }

    Enum and Collection of Enum parameter

    Configuration

    In BuildAction(), we can configure an action with Enum and collection of Enum parameters:

    var action = builder.EntityType<Customer>().Collection.Action("EnumAction");
    action.Parameter<Color>("e1");
    action.Parameter<Color?>("e2"); // nullable
    action.CollectionParameter<Color?>("e3"); // Collection of nullable

    Routing

    In the CustomersController, add the following method :

    [HttpPost]
    public IHttpActionResult EnumAction(ODataActionParameters parameters)
    {
      ...
    }

    Request Samples

    We can invoke the action by issuing a Post on ~/odata/Customers/Default.EnumAction with the following request body:

    {
      "e1":"Red",
      "e2":"Green",
      "e3":["Red", null, "Blue"]
    }

    Complex and Collection of Complex parameter

    Configuration

    In BuildAction(), we can configure an action with Complex and collection of Complex parameters:

    var action = builder.EntityType<Customer>().Collection.Action("ComplexAction").Returns<string>();
    action.Parameter<Address>("address");
    action.CollectionParameter<Address>("addresses");

    Routing

    In the CustomersController, add the following method :

    [HttpPost]
    public IHttpActionResult ComplexAction(ODataActionParameters parameters)
    {
      ...
    }

    Request Samples

    We can invoke the action by issuing a Post on ~/odata/Customers/Default.ComplexAction with the following request body:

    {
      "address":{"City":"Redmond"},
      "addresses":[{"@odata.type":"#NS.Address","City":"Redmond"},{"@odata.type":"#NS.SubAddress","City":"Shanghai","Street":"Zi Xing Rd"}]
    }

    Entity and Collection of Entity parameter

    Configuration

    In BuildAction(), we can configure an action with Entity and collection of Entity parameters:

    var action = builder.EntityType<Customer>().Collection.Action("EntityAction").Returns<string>();
    action.EntityParameter<Customer>("customer");
    action.CollectionEntityParameter<Customer>("customers"); 

    It’s better to call EntityParameter<T> and CollectionEntityParameter<T> to define entity and collection of entity parameter.

    Routing

    In the CustomersController, add the following method :

    [HttpPost]
    public IHttpActionResult EntityAction(ODataActionParameters parameters)
    {
      ...
    }

    Request Samples

    We can invoke the action by issuing a Post on ~/odata/Customers/Default.EntityAction with the following request body:

    {
      "customer":{\"@odata.type\":\"#NS.Customer\",\"Id\":1,\"Name\":\"John\"},
      "customers":[
        {\"@odata.type\":\"#NS.Customer\",\"Id\":2, \"Name\":\"Mike\"},
        {\"@odata.type\":\"#NS.SubCustomer\",\"Id\":3,\"Name\":\"Tony\", \"Price\":9.9}
      ]
    }

    Know issues

    1. It doesn’t work if “null” value in the collection of entity in the payload. See detail in #100.

    2. It doesn’t work if anything else follows up the collection of entity in the payload. See detail in #65

    Null value

    If you invoke an action with a ‘null’ action parameter value, please don’t add the parameter (for example, "p1":null) in the payload and leave it un-specified. However, for collection, you should always specify it even the collection is an empty collection (for example, "p1":[]).

    Thanks.

    For un-typed scenario, please refer to untyped page.

  • 4.8 Operation paramters in untyped scenarios

    In this page, we introduce the Function/Action parameter in untyped scenario. For CLR typed scenarios, please refer to Function page and Action page.

    Build Edm Model

    Let’s build the Edm Model from scratch:

    private static IEdmModel GetEdmModel()
    {
        EdmModel model = new EdmModel();
    
        // Enum type "Color"
        EdmEnumType colorEnum = new EdmEnumType("NS", "Color");
        colorEnum.AddMember(new EdmEnumMember(colorEnum, "Red", new EdmIntegerConstant(0)));
        colorEnum.AddMember(new EdmEnumMember(colorEnum, "Blue", new EdmIntegerConstant(1)));
        colorEnum.AddMember(new EdmEnumMember(colorEnum, "Green", new EdmIntegerConstant(2)));
        model.AddElement(colorEnum);
    
        // complex type "Address"
        EdmComplexType address = new EdmComplexType("NS", "Address");
        address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String);
        address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String);
        model.AddElement(address);
    
        // derived complex type "SubAddress"
        EdmComplexType subAddress = new EdmComplexType("NS", "SubAddress", address);
        subAddress.AddStructuralProperty("Code", EdmPrimitiveTypeKind.Double);
        model.AddElement(subAddress);
    
        // entity type "Customer"
        EdmEntityType customer = new EdmEntityType("NS", "Customer");
        customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32));
        customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String);
        model.AddElement(customer);
    
        // derived entity type special customer
        EdmEntityType subCustomer = new EdmEntityType("NS", "SubCustomer", customer);
        subCustomer.AddStructuralProperty("Price", EdmPrimitiveTypeKind.Double);
        model.AddElement(subCustomer);
    
        // entity sets
        EdmEntityContainer container = new EdmEntityContainer("NS", "Default");
        model.AddElement(container);
        container.AddEntitySet("Customers", customer);
    
        IEdmTypeReference intType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: true);
        EdmEnumTypeReference enumType = new EdmEnumTypeReference(colorEnum, isNullable: true);
        EdmComplexTypeReference complexType = new EdmComplexTypeReference(address, isNullable: true);
        EdmEntityTypeReference entityType = new EdmEntityTypeReference(customer, isNullable: true);
    
        // functions
        BuildFunction(model, "PrimitiveFunction", entityType, "param", intType);
        BuildFunction(model, "EnumFunction", entityType, "color", enumType);
        BuildFunction(model, "ComplexFunction", entityType, "address", complexType);
        BuildFunction(model, "EntityFunction", entityType, "customer", entityType);
        
        // actions
        BuildAction(model, "PrimitiveAction", entityType, "param", intType);
        BuildAction(model, "EnumAction", entityType, "color", enumType);
        BuildAction(model, "ComplexAction", entityType, "address", complexType);
        BuildAction(model, "EntityAction", entityType, "customer", entityType);
        return model;
    }
    
    private static void BuildFunction(EdmModel model, string funcName, IEdmEntityTypeReference bindingType, string paramName, IEdmTypeReference edmType)
    {
        IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false);
    
        EdmFunction boundFunction = new EdmFunction("NS", funcName, returnType, isBound: true, entitySetPathExpression: null, isComposable: false);
        boundFunction.AddParameter("entity", bindingType);
        boundFunction.AddParameter(paramName, edmType);
        boundFunction.AddParameter(paramName + "List", new EdmCollectionTypeReference(new EdmCollectionType(edmType)));
        model.AddElement(boundFunction);
    }
    
    private static void BuildAction(EdmModel model, string actName, IEdmEntityTypeReference bindingType, string paramName, IEdmTypeReference edmType)
    {
        IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false);
    
        EdmAction boundAction = new EdmAction("NS", actName, returnType, isBound: true, entitySetPathExpression: null);
        boundAction.AddParameter("entity", bindingType);
        boundAction.AddParameter(paramName, edmType);
        boundAction.AddParameter(paramName + "List", new EdmCollectionTypeReference(new EdmCollectionType(edmType)));
        model.AddElement(boundAction);
    }

    Here’s the metadata document for this Edm Model:

    <?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="NS" xmlns="http://docs.oasis-open.org/odata/ns/edm">
          <EnumType Name="Color">
            <Member Name="Red" Value="0" />
            <Member Name="Blue" Value="1" />
            <Member Name="Green" Value="2" />
          </EnumType>
          <ComplexType Name="Address">
            <Property Name="Street" Type="Edm.String" />
            <Property Name="City" Type="Edm.String" />
          </ComplexType>
          <ComplexType Name="SubAddress" BaseType="NS.Address">
            <Property Name="Code" Type="Edm.Double" />
          </ComplexType>
          <EntityType Name="Customer">
            <Key>
              <PropertyRef Name="Id" />
            </Key>
            <Property Name="Id" Type="Edm.Int32" />
            <Property Name="Name" Type="Edm.String" />
          </EntityType>
          <EntityType Name="SubCustomer" BaseType="NS.Customer">
            <Property Name="Price" Type="Edm.Double" />
          </EntityType>
          <Function Name="PrimitiveFunction" IsBound="true">
            <Parameter Name="entity" Type="NS.Customer" />
            <Parameter Name="param" Type="Edm.Int32" />
            <Parameter Name="paramList" Type="Collection(Edm.Int32)" />
            <ReturnType Type="Edm.Boolean" Nullable="false" />
          </Function>
          <Function Name="EnumFunction" IsBound="true">
            <Parameter Name="entity" Type="NS.Customer" />
            <Parameter Name="color" Type="NS.Address" />
            <Parameter Name="colorList" Type="Collection(NS.Color)" />
            <ReturnType Type="Edm.Boolean" Nullable="false" />
          </Function>
          <Function Name="ComplexFunction" IsBound="true">
            <Parameter Name="entity" Type="NS.Customer" />
            <Parameter Name="address" Type="NS.Address" />
            <Parameter Name="addressList" Type="Collection(NS.Address)" />
            <ReturnType Type="Edm.Boolean" Nullable="false" />
          </Function>
          <Function Name="EntityFunction" IsBound="true">
            <Parameter Name="entity" Type="NS.Customer" />
            <Parameter Name="customer" Type="NS.Color" />
            <Parameter Name="customerList" Type="Collection(NS.Customer)" />
            <ReturnType Type="Edm.Boolean" Nullable="false" />
          </Function>
          <Action Name="PrimitiveAction" IsBound="true">
            <Parameter Name="entity" Type="NS.Customer" />
            <Parameter Name="param" Type="Edm.Int32" />
            <Parameter Name="paramList" Type="Collection(Edm.Int32)" />
            <ReturnType Type="Edm.Boolean" Nullable="false" />
          </Action>
          <Action Name="EnumAction" IsBound="true">
            <Parameter Name="entity" Type="NS.Customer" />
            <Parameter Name="color" Type="NS.Address" />
            <Parameter Name="colorList" Type="Collection(NS.Color)" />
            <ReturnType Type="Edm.Boolean" Nullable="false" />
          </Action>
          <Action Name="ComplexAction" IsBound="true">
            <Parameter Name="entity" Type="NS.Customer" />
            <Parameter Name="address" Type="NS.Address" />
            <Parameter Name="addressList" Type="Collection(NS.Address)" />
            <ReturnType Type="Edm.Boolean" Nullable="false" />
          </Action>
          <Action Name="EntityAction" IsBound="true">
            <Parameter Name="entity" Type="NS.Customer" />
            <Parameter Name="customer" Type="NS.Color" />
            <Parameter Name="customerList" Type="Collection(NS.Customer)" />
            <ReturnType Type="Edm.Boolean" Nullable="false" />
          </Action>
          <EntityContainer Name="Default">
            <EntitySet Name="Customers" EntityType="NS.Customer" />
          </EntityContainer>
        </Schema>
      </edmx:DataServices>
    </edmx:Edmx>

    Controller & Routing

    Let’s add the following methods into CustomersController:

    [HttpGet]
    public IHttpActionResult PrimitiveFunction(int key, int? param, [FromODataUri]IList<int?> paramList)
    {
        ......
    }
    
    [HttpPost]
    public IHttpActionResult PrimitiveAction(int key, ODataActionParameters parameters)
    {
        ......
    }
    
    /* // will support in V5.5 RTM
    [HttpGet]
    public IHttpActionResult EnumFunction(int key, [FromODataUri]EdmEnumObject color, [FromODataUri]EdmEnumObjectCollection colorList)
    {
        ......
    }
    
    [HttpPost]
    public IHttpActionResult EnumAction(int key, ODataActionParameters parameters)
    {
        ......
    }
    */
    
    [HttpGet]
    public IHttpActionResult ComplexFunction(int key, [FromODataUri]EdmComplexObject address, [FromODataUri]EdmComplexObjectCollection addressList)
    {
        ......
    }
    
    [HttpPost]
    public IHttpActionResult ComplexAction(int key, ODataActionParameters parameters)
    {
        ......
    }
    
    [HttpGet]
    public IHttpActionResult EntityFunction(int key, [FromODataUri]EdmEntityObject customer, [FromODataUri]EdmEntityObjectCollection customerList)
    {
        ......
    }
    
    [HttpPost]
    public IHttpActionResult EntityAction(int key, ODataActionParameters parameters)
    {
        ......
    }

    Request Samples

    Now, We can invoke the function with the entity and collection of entity parameter as:

    ~odata/Customers(1)/NS.EntityFunction(customer=@x,customerList=@y)?@x={\"@odata.type\":\"%23NS.Customer\",\"Id\":1,\"Name\":\"John\"}&@y={\"value\":[{\"@odata.type\":\"%23NS.Customer\",\"Id\":2, \"Name\":\"Mike\"},{\"@odata.type\":\"%23NS.SubCustomer\",\"Id\":3,\"Name\":\"Tony\", \"Price\":9.9}]}"

    Also, We can invoke the action by issuing a Post on ~/odata/Customers(1)/NS.EntityAction with the following request body:

    {
      "customer":{\"@odata.type\":\"#NS.Customer\",\"Id\":1,\"Name\":\"John\"},
      "customerList":[
        {\"@odata.type\":\"#NS.Customer\",\"Id\":2, \"Name\":\"Mike\"},
        {\"@odata.type\":\"#NS.SubCustomer\",\"Id\":3,\"Name\":\"Tony\", \"Price\":9.9}
      ]
    }

    For other request samples, please refer to Function page and Action page.

    Unbound function/action

    Unbound function and action are similiar with bound function and action in the request format. But only attribute routing can be used for unbound function/action routing.

    Thanks.

  • 4.9 Query by dynamic properties

    Since Web API OData V5.5, it supports filter, select and orderby on dynamic properties.

    Let’s see a sample about this feature.

    CLR Model

    First of all, we create the following CLR classes as our model:

    public class SimpleOpenCustomer
    {
        [Key]
        public int CustomerId { get; set; }
        public string Name { get; set; }
        public string Website { get; set; }
        public IDictionary<string, object> CustomerProperties { get; set; }
    }

    Build Edm Model

    Now, we can build the Edm Model as:

    private static IEdmModel GetEdmModel()
    { 
        ODataModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<SimpleOpenCustomer>("SimpleOpenCustomers");
        return builder.GetEdmModel();
    }

    Use filter, orferby, select on dynamic property

    Routing

    In the SimpleOpenCustomersController, add the following method:

    [EnableQuery]
    public IQueryable<SimpleOpenCustomer> Get()
    {
        return CreateCustomers().AsQueryable();
    }

    Request Samples

    We can query like:

    ~/odata/SimpleOpenCustomers?$orderby=Token desc&$filter=Token ne null
    ~/odata/SimpleOpenCustomers?$select=Token
  • 4.10 Open type in untyped scenarios

    Since Web API OData V5.5, it supports open type and dynamic property on un-type scenario, dynamic properties can be:

    1. Primitive Type
    2. Enum Type
    3. Complex Type
    4. Collection of above

    Let’s see a sample about this feature.

    Build un-type Edm Model

    Now, we can build the Edm Model as:

    private static IEdmModel GetUntypedEdmModel()
    {
        var model = new EdmModel();
        // complex type address
        EdmComplexType address = new EdmComplexType("NS", "Address", null, false, true);
        address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String);
        model.AddElement(address);
        // enum type color
        EdmEnumType color = new EdmEnumType("NS", "Color");
        color.AddMember(new EdmEnumMember(color, "Red", new EdmIntegerConstant(0)));
        model.AddElement(color);
        // entity type customer
        EdmEntityType customer = new EdmEntityType("NS", "UntypedSimpleOpenCustomer", null, false, true);
        customer.AddKeys(customer.AddStructuralProperty("CustomerId", EdmPrimitiveTypeKind.Int32));
        customer.AddStructuralProperty("Color", new EdmEnumTypeReference(color, isNullable: true));
        model.AddElement(customer);  
        EdmEntityContainer container = new EdmEntityContainer("NS", "Container");
        container.AddEntitySet("UntypedSimpleOpenCustomers", customer);
        model.AddElement(container);
        return model;
    }

    If the dynamic property is not primitive type, you should declare it in model like the code above.

    GET an untyped open entity with dynamic property

    Routing

    In the UntypedSimpleOpenCustomersController, add the following method:

    [HttpGet]
    public IHttpActionResult Get(int key)
    {
       //return an EdmEntityObject
       ...
    }

    Request Samples

    We can get the entity as:

    ~/odata/UntypedSimpleOpenCustomers(1)

    POST an untyped open entity with dynamic property

    Routing

    In the UntypedSimpleOpenCustomersController, add the following method :

    [HttpPost]
    public IHttpActionResult PostUntypedSimpleOpenCustomer(EdmEntityObject customer)
    {
        object nameValue;
        customer.TryGetPropertyValue("Name", out nameValue);
        Type nameType;
        customer.TryGetPropertyType("Name", out nameType);
        ...
    }

    Request Samples

    You should declare the type of dynamic properties in request body. Payload:

    const string Payload = "{" + 
                  "\"@odata.context\":\"http://localhost/odata/$metadata#UntypedSimpleOpenCustomer/$entity\"," +
                  "\"CustomerId\":6,\"Name\":\"FirstName 6\"," +
                  "\"Address\":{" +
                    "\"@odata.type\":\"#NS.Address\",\"Street\":\"Street 6\",\"City\":\"City 6\"" +
                  "}," + 
                  "\"Addresses@odata.type\":\"#Collection(NS.Address)\"," +
                  "\"Addresses\":[{" +
                    "\"@odata.type\":\"#NS.Address\",\"Street\":\"Street 7\",\"City\":\"City 7\"" +
                  "}]," +
                  "\"DoubleList@odata.type\":\"#Collection(Double)\"," +
                  "\"DoubleList\":[5.5, 4.4, 3.3]," +
                  "\"FavoriteColor@odata.type\":\"#NS.Color\"," +
                  "\"FavoriteColor\":\"Red\"," +
                  "\"Color\":\"Red\"," +
                  "\"FavoriteColors@odata.type\":\"#Collection(NS.Color)\"," +
                  "\"FavoriteColors\":[\"0\", \"1\"]" +
                "}";

    Url:

    ~/odata/UntypedSimpleOpenCustomers

    The type of dynamic properties in un-type scenario

    EdmEntityObject (Collection)

    Represent an entity.

    EdmComplexObject (Collection)

    Represent an complex property.

    EdmEnumObject (Collection)

    Represent an enum property.

  • 4.11 Query Options

  • 4.12 Batch Support

    Batch requests allow grouping multiple operations into a single HTTP request payload and the service will return a single HTTP response with the response to all operations in the requests. This way, the client can optimize calls to the server and improve the scalability of its service.

    Please refer to OData Protocol for more detail about batch, and Batch in ODL for batch in ODL client.

    Enable Batch in Web API OData Service

    It is very easy to enable batch in an OData service which is built by Web API OData.

    Add Batch Handler

    public static void Register(HttpConfiguration config)
    {
        var odataBatchHandler = new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer);
        config.MapODataServiceRoute("odata", "odata", GetModel(), odataBatchHandler);
    }

    As above, we only need to create a new batch handler and pass it when mapping routing for OData service. Batch will be enabled.

    For testing, we can POST a request with batch body to the baseurl/$batch:

    POST http://localhost:14409/odata/$batch HTTP/1.1
    User-Agent: Fiddler
    Host: localhost:14409
    Content-Length: 1244
    Content-Type: multipart/mixed;boundary=batch_d3bcb804-ee77-4921-9a45-761f98d32029
    
    --batch_d3bcb804-ee77-4921-9a45-761f98d32029
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    
    GET http://localhost:14409/odata/Products(0)  HTTP/1.1
    OData-Version: 4.0
    OData-MaxVersion: 4.0
    Accept: application/json;odata.metadata=minimal
    Accept-Charset: UTF-8
    User-Agent: Microsoft ADO.NET Data Services
    
    --batch_d3bcb804-ee77-4921-9a45-761f98d32029
    Content-Type: multipart/mixed;boundary=changeset_77162fcd-b8da-41ac-a9f8-9357efbbd
    
    --changeset_77162fcd-b8da-41ac-a9f8-9357efbbd 
    Content-Type: application/http 
    Content-Transfer-Encoding: binary 
    Content-ID: 1
    
    DELETE http://localhost:14409/odata/Products(0) HTTP/1.1
    OData-Version: 4.0
    OData-MaxVersion: 4.0
    Accept: application/json;odata.metadata=minimal
    Accept-Charset: UTF-8
    User-Agent: Microsoft ADO.NET Data Services
    
    --changeset_77162fcd-b8da-41ac-a9f8-9357efbbd--
    --batch_d3bcb804-ee77-4921-9a45-761f98d32029
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    
    GET http://localhost:14409/odata/Products  HTTP/1.1
    OData-Version: 4.0
    OData-MaxVersion: 4.0
    Accept: application/json;odata.metadata=minimal
    Accept-Charset: UTF-8
    User-Agent: Microsoft ADO.NET Data Services
    
    --batch_d3bcb804-ee77-4921-9a45-761f98d32029--
    

    And the response should be:

    HTTP/1.1 200 OK
    Cache-Control: no-cache
    Pragma: no-cache
    Content-Type: multipart/mixed; boundary=batchresponse_5667121d-ca2f-458d-9bae-172f04cdd411
    Expires: -1
    Server: Microsoft-IIS/8.0
    OData-Version: 4.0
    X-AspNet-Version: 4.0.30319
    X-SourceFiles: =?UTF-8?B?YzpcdXNlcnNcbGlhbndcZG9jdW1lbnRzXHZpc3VhbCBzdHVkaW8gMjAxM1xQcm9qZWN0c1xUZXN0V2ViQVBJUmVsZWFzZVxUZXN0V2ViQVBJUmVsZWFzZVxvZGF0YVwkYmF0Y2g=?=
    X-Powered-By: ASP.NET
    Date: Wed, 06 May 2015 07:34:29 GMT
    Content-Length: 1449
    
    --batchresponse_5667121d-ca2f-458d-9bae-172f04cdd411
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    
    HTTP/1.1 200 OK
    Content-Type: application/json; odata.metadata=minimal; charset=utf-8
    OData-Version: 4.0
    
    {
      "@odata.context":"http://localhost:14409/odata/$metadata#Products/$entity","ID":0,"Name":"0Name"
    }
    --batchresponse_5667121d-ca2f-458d-9bae-172f04cdd411
    Content-Type: multipart/mixed; boundary=changesetresponse_e2f20275-a425-404a-8f01-c9818aa63610
    
    --changesetresponse_e2f20275-a425-404a-8f01-c9818aa63610
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    Content-ID: 1
    
    HTTP/1.1 204 No Content
    
    
    --changesetresponse_e2f20275-a425-404a-8f01-c9818aa63610--
    --batchresponse_5667121d-ca2f-458d-9bae-172f04cdd411
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    
    HTTP/1.1 200 OK
    Content-Type: application/json; odata.metadata=minimal; charset=utf-8
    OData-Version: 4.0
    
    {
      "@odata.context":"http://localhost:14409/odata/$metadata#Products","value":[
        {
          "ID":1,"Name":"1Name"
        },{
          "ID":2,"Name":"2Name"
        },{
          "ID":3,"Name":"3Name"
        },{
          "ID":4,"Name":"4Name"
        },{
          "ID":5,"Name":"5Name"
        },{
          "ID":6,"Name":"6Name"
        },{
          "ID":7,"Name":"7Name"
        },{
          "ID":8,"Name":"8Name"
        },{
          "ID":9,"Name":"9Name"
        }
      ]
    }
    --batchresponse_5667121d-ca2f-458d-9bae-172f04cdd411--
    

    Setting Batch Quotas

    DefaultODataBatchHandler contains some configuration, which can be set by customers, to customize the handler. For example, the following code will only allow a maximum of 8 requests per batch and 5 operations per ChangeSet.

    var odataBatchHandler = new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer);
    odataBatchHandler.MessageQuotas.MaxPartsPerBatch = 8;
    odataBatchHandler.MessageQuotas.MaxOperationsPerChangeset = 5;

    Enable/Disable continue-on-error in Batch Request

    We can handle the behavior upon encountering a request within the batch that returns an error by preference odata.continue-on-error, which is specified by OData V4 spec.

    Enable Preference odata.continue-on-error

    Preference odata.continue-on-error makes no sense by default, and service returns the error for that request and continue processing additional requests within the batch as default behavior.

    To enable odata.continue-on-error, please refer to section 4.20 Prefer odata.continue-on-error for details.

    Request Without Preference odata.continue-on-error

    For testing, we can POST a batch request without Preference odata.continue-on-error:

    POST http://localhost:9001/DefaultBatch/$batch HTTP/1.1
    Accept: multipart/mixed
    Content-Type: multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0
    Host: localhost:9001
    Content-Length: 633
    Expect: 100-continue
    Connection: Keep-Alive
    
    --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    
    GET http://localhost:9001/DefaultBatch/DefaultBatchCustomer(0) HTTP/1.1
    
    --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    
    GET http://localhost:9001/DefaultBatch/DefaultBatchCustomerfoo HTTP/1.1
    
    --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    
    GET http://localhost:9001/DefaultBatch/DefaultBatchCustomer(1) HTTP/1.1
    
    --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0--
    

    The response should be:

    HTTP/1.1 200 OK
    Content-Length: 820
    Content-Type: multipart/mixed; boundary=batchresponse_b49114d7-62f7-450a-8064-e27ef9562eda
    Server: Microsoft-HTTPAPI/2.0
    OData-Version: 4.0
    Date: Wed, 12 Aug 2015 02:23:10 GMT
    
    --batchresponse_b49114d7-62f7-450a-8064-e27ef9562eda
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    
    HTTP/1.1 200 OK
    Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
    OData-Version: 4.0
    
    {
      "@odata.context":"http://localhost:9001/DefaultBatch/$metadata#DefaultBatchCustomer/$entity","Id":0,"Name":"Name 0"
    }
    --batchresponse_b49114d7-62f7-450a-8064-e27ef9562eda
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    
    HTTP/1.1 404 Not Found
    Content-Type: application/json; charset=utf-8
    
    {"Message":"No HTTP resource was found that matches the request URI 'http://localhost:9001/DefaultBatch/DefaultBatchCustomerfoo'.","MessageDetail":"No route data was found for this request."}
    --batchresponse_b49114d7-62f7-450a-8064-e27ef9562eda--
    

    Service returned error and stop processing.

    Request With Preference odata.continue-on-error

    Now POST a batch request with Preference odata.continue-on-error:

    POST http://localhost:9001/DefaultBatch/$batch HTTP/1.1
    Accept: multipart/mixed
    prefer: odata.continue-on-error
    Content-Type: multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0
    Host: localhost:9001
    Content-Length: 633
    Expect: 100-continue
    Connection: Keep-Alive
    
    --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    
    GET http://localhost:9001/DefaultBatch/DefaultBatchCustomer(0) HTTP/1.1
    
    --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    
    GET http://localhost:9001/DefaultBatch/DefaultBatchCustomerfoo HTTP/1.1
    
    --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    
    GET http://localhost:9001/DefaultBatch/DefaultBatchCustomer(1) HTTP/1.1
    
    --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0--
    

    Service returns the error for that request and continue processing additional requests within the batch:

    HTTP/1.1 200 OK
    Content-Length: 1190
    Content-Type: multipart/mixed; boundary=batchresponse_60fec4c2-3ce7-4900-a05a-93f180629a11
    Server: Microsoft-HTTPAPI/2.0
    OData-Version: 4.0
    Date: Wed, 12 Aug 2015 02:27:45 GMT
    
    --batchresponse_60fec4c2-3ce7-4900-a05a-93f180629a11
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    
    HTTP/1.1 200 OK
    Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
    OData-Version: 4.0
    
    {
      "@odata.context":"http://localhost:9001/DefaultBatch/$metadata#DefaultBatchCustomer/$entity","Id":0,"Name":"Name 0"
    }
    --batchresponse_60fec4c2-3ce7-4900-a05a-93f180629a11
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    
    HTTP/1.1 404 Not Found
    Content-Type: application/json; charset=utf-8
    
    {"Message":"No HTTP resource was found that matches the request URI 'http://localhost:9001/DefaultBatch/DefaultBatchCustomerfoo'.","MessageDetail":"No route data was found for this request."}
    --batchresponse_60fec4c2-3ce7-4900-a05a-93f180629a11
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    
    HTTP/1.1 200 OK
    Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
    OData-Version: 4.0
    
    {
      "@odata.context":"http://localhost:9001/DefaultBatch/$metadata#DefaultBatchCustomer/$entity","Id":1,"Name":"Name 1"
    }
    --batchresponse_60fec4c2-3ce7-4900-a05a-93f180629a11--
    
  • 4.13 Delta Feed Support

    Serialization Support for Delta Feed

    This sample will introduce how to create a Delta Feed which is serialized into a Delta Response in Web API OData V4.

    Similar to EdmEntityObjectCollection, Web API OData V5.6 now has an EdmChangedObjectCollection to represent a collection of objects which can be a part of the Delta Feed. A delta response can contain new/changed entities, deleted entities, new links or deleted links.

    WebAPI OData V4 now has EdmDeltaEntityObject, EdmDeltaDeletedEntityObject, EdmDeltaLink and EdmDeltaDeletedLink respectively for the objects that can be a part of the Delta response. All the above objects implement the IEdmChangedObject interface, while the EdmChangedObjectCollection is a collection of IEdmChangedObject.

    For example, if user defines a model as:

    public class Customer
    {
      public int Id { get; set; }
      public string Name { get; set; }
      public virtual IList<Order> Orders { get; set; }
    }
    public class Order
    {
      public int Id { get; set; }
      public string ShippingAddress { get; set; }
    }
    private static IEdmModel GetEdmModel()
    {
      ODataModelBuilder builder = new ODataConventionModelBuilder();
      var customers = builder.EntitySet<Customer>("Customers");
      var orders = builder.EntitySet<Order>("Orders");
      return builder.GetEdmModel();
    }

    The EdmChangedObjectCollection collection for Customer entity will be created as follows:

    EdmChangedObjectCollection changedCollection = new EdmChangedObjectCollection(CustomerType); //IEdmEntityType of Customer

    Changed or Modified objects are added as EdmDeltaEntityObjects:

    EdmDeltaEntityObject Customer = new EdmDeltaEntityObject(CustomerType); 
    Customer.Id = 123;
    Customer.Name = "Added Customer";
    changedCollection.Add(Customer);

    Deleted objects are added as EdmDeltaDeletedObjects:

    EdmDeltaDeletedEntityObject Customer = new EdmDeltaDeletedEntityObject(CustomerType);
    Customer.Id = 123;
    Customer.Reason = DeltaDeletedEntryReason.Deleted;
    changedCollection.Add(Customer);

    Delta Link is added corresponding to a $expand in the initial request, these are added as EdmDeltaLinks:

    EdmDeltaLink CustomerOrderLink = new EdmDeltaLink(CustomerType);
    CustomerOrderLink.Relationship = "Orders";
    CustomerOrderLink.Source = new Uri(ServiceBaseUri, "Customers(123)");	
    CustomerOrderLink.Target = new Uri(ServiceBaseUri, "Orders(10)");
    changedCollection.Add(CustomerOrderLink);

    Deleted Links is added for each deleted link that corresponds to a $expand path in the initial request, these are added as EdmDeltaDeletedLinks:

    EdmDeltaDeletedLink CustomerOrderDeletedLink = new EdmDeltaDeletedLink(CustomerType);
    CustomerOrderDeletedLink.Relationship = "Orders";
    CustomerOrderDeletedLink.Source = new Uri(ServiceBaseUri, "Customers(123)");
    CustomerOrderDeletedLink.Target = new Uri(ServiceBaseUri, "Orders(10)");
    changedCollection.Add(CustomerOrderDeletedLink);

    Sample for Delta Feed

    Let’s create a controller to return a Delta Feed:

    public class CustomersController : ODataController
    {
      public IHttpActionResult Get()
      {
        EdmChangedObjectCollection changedCollection = new EdmChangedObjectCollection(CustomerType);
        EdmDeltaEntityObject Customer = new EdmDeltaEntityObject(CustomerType); 
        Customer.Id = 123;
        Customer.Name = "Added Customer";
        changedCollection.Add(Customer);
        
        EdmDeltaDeletedEntityObject Customer = new EdmDeltaDeletedEntityObject(CustomerType);
        Customer.Id = 124;
        Customer.Reason = DeltaDeletedEntryReason.Deleted;
        changedCollection.Add(Customer);
    
        EdmDeltaLink CustomerOrderLink = new EdmDeltaLink(CustomerType);
        CustomerOrderLink.Relationship = "Orders";
        CustomerOrderLink.Source = new Uri(ServiceBaseUri, "Customers(123)");
        CustomerOrderLink.Target = new Uri(ServiceBaseUri, "Orders(10)");
        changedCollection.Add(CustomerOrderLink);
        
        EdmDeltaDeletedLink CustomerOrderDeletedLink = new EdmDeltaDeletedLink(CustomerType);
        CustomerOrderDeletedLink.Relationship = "Orders";
        CustomerOrderDeletedLink.Source = new Uri(ServiceBaseUri, "Customers(123)");
        CustomerOrderDeletedLink.Target = new Uri(ServiceBaseUri, "Orders(11)");
        changedCollection.Add(CustomerOrderDeletedLink);
        return Ok(changedCollection);
      }
    }

    Now, user can issue a GET request as:

    http://localhost/odata/Customers?$expand=Orders&$deltatoken=abc

    The corresponding payload will has the following contents:

    {
      "@odata.context":"http://localhost/odata/$metadata#Customers",
      "value": [
        {
          "Id":123,
          "Name":"Added Customer"
        },
        {
          "@odata.context":"http://localhost/odata/$metadata#Customers/$deletedEntity",
          "Id": 124
          "Reason":"Deleted"
        },
        {
          "@odata.context":"http://localhost/odata/$metadata#Customers/$link",
          "source":"Customers(123)",
          "relationship":"Orders",
          "target":"Orders(10)"
        },
        {
         	"@odata.context":"http://localhost/odata/$metadata#Customers/$deletedLink",
         	"source":"Customers(123)",
         	"relationship":"Orders",
         	"target":"Orders(11)"
        }
      ]
    }
  • 4.14 Capabilities vocabulary support

    Web API OData supports some query limitations, for example:

    • NonFilterable / NotFilterable – $filter
    • NotCountable – $count
    • NotExpandable – $expand
    • NotNavigable – $select
    • NotSortable / Unsortable – $orderby

    However, the corresponding annotations cannot be exposed in metadata document. This sample introduces the capabilities vocabulary support in Web API OData V5.7, which will enable capabilites vocabulary annotations in metadata document. The related sample codes can be found here.

    Build Edm Model

    Let’s define a model with query limitations:

    public class Customer
    {
    	public int CustomerId { get; set; }
    
    	public string Name { get; set; }
    
    	[NotFilterable]
    	[NotSortable]
    	public Guid Token { get; set; }
    
    	[NotNavigable]
    	public string Email { get; set; }
    
    	[NotCountable]
    	public IList<Address> Addresses { get; set; }
    
    	[NotCountable]
    	public IList<Color> FavoriateColors { get; set; }
    
    	[NotExpandable]
    	public IEnumerable<Order> Orders { get; set; }
    }

    Where, Address is a normal complex type, Color is an enum type and Order is a normal entity type. You can find their definitions in the sample codes.

    Based on the above CLR classes, we can build the Edm model as:

    private static IEdmModel GetEdmModel()
    {
    	var builder = new ODataConventionModelBuilder();
    	builder.EntitySet<Customer>("Customers");
    	builder.EntitySet<Order>("Orders");
    	return builder.GetEdmModel();
    }

    Expose annotations

    Now, you can query the metadata document for capabilites vocabulary annotation as:

    <?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="CapabilitiesVocabulary" xmlns="http://docs.oasis-open.org/odata/ns/edm">
          <EntityType Name="Customer">
            <Key>
              <PropertyRef Name="CustomerId" />
            </Key>
            <Property Name="CustomerId" Type="Edm.Int32" Nullable="false" />
            <Property Name="Name" Type="Edm.String" />
            <Property Name="Token" Type="Edm.Guid" Nullable="false" />
            <Property Name="Email" Type="Edm.String" />
            <Property Name="Addresses" Type="Collection(CapabilitiesVocabulary.Address)" />
            <Property Name="FavoriateColors" Type="Collection(CapabilitiesVocabulary.Color)" Nullable="false" />
            <NavigationProperty Name="Orders" Type="Collection(CapabilitiesVocabulary.Order)" />
          </EntityType>
          <EntityType Name="Order">
            <Key>
              <PropertyRef Name="OrderId" />
            </Key>
            <Property Name="OrderId" Type="Edm.Int32" Nullable="false" />
            <Property Name="Price" Type="Edm.Double" Nullable="false" />
          </EntityType>
          <ComplexType Name="Address">
            <Property Name="City" Type="Edm.String" />
            <Property Name="Street" Type="Edm.String" />
          </ComplexType>
          <EnumType Name="Color">
            <Member Name="Red" Value="0" />
            <Member Name="Green" Value="1" />
            <Member Name="Blue" Value="2" />
            <Member Name="Yellow" Value="3" />
            <Member Name="Pink" Value="4" />
            <Member Name="Purple" Value="5" />
          </EnumType>
        </Schema>
        <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
          <EntityContainer Name="Container">
            <EntitySet Name="Customers" EntityType="CapabilitiesVocabulary.Customer">
              <NavigationPropertyBinding Path="Orders" Target="Orders" />
              <Annotation Term="Org.OData.Capabilities.V1.CountRestrictions">
                <Record>
                  <PropertyValue Property="Countable" Bool="true" />
                  <PropertyValue Property="NonCountableProperties">
                    <Collection>
                      <PropertyPath>Addresses</PropertyPath>
                      <PropertyPath>FavoriateColors</PropertyPath>
                    </Collection>
                  </PropertyValue>
                  <PropertyValue Property="NonCountableNavigationProperties">
                    <Collection />
                  </PropertyValue>
                </Record>
              </Annotation>
              <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>Token</PropertyPath>
                    </Collection>
                  </PropertyValue>
                </Record>
              </Annotation>
              <Annotation Term="Org.OData.Capabilities.V1.SortRestrictions">
                <Record>
                  <PropertyValue Property="Sortable" Bool="true" />
                  <PropertyValue Property="AscendingOnlyProperties">
                    <Collection />
                  </PropertyValue>
                  <PropertyValue Property="DescendingOnlyProperties">
                    <Collection />
                  </PropertyValue>
                  <PropertyValue Property="NonSortableProperties">
                    <Collection>
                      <PropertyPath>Token</PropertyPath>
                    </Collection>
                  </PropertyValue>
                </Record>
              </Annotation>
              <Annotation Term="Org.OData.Capabilities.V1.ExpandRestrictions">
                <Record>
                  <PropertyValue Property="Expandable" Bool="true" />
                  <PropertyValue Property="NonExpandableProperties">
                    <Collection>
                      <NavigationPropertyPath>Orders</NavigationPropertyPath>
                    </Collection>
                  </PropertyValue>
                </Record>
              </Annotation>
            </EntitySet>
            <EntitySet Name="Orders" EntityType="CapabilitiesVocabulary.Order" />
          </EntityContainer>
        </Schema>
      </edmx:DataServices>
    </edmx:Edmx>

    Thanks.

  • 4.15 AutoExpand attribute

    In OData WebApi 5.7, we can put AutoExpand attribute on navigation property to make it automatically expand without expand query option, or can put this attribute on class to make all Navigation Property on this class automatically expand.

    Model

    public class Product
    {
        public int Id { get; set; }
        [AutoExpand]
        public Category Category { get; set; }
    }
    
    public class Category
    {
        public int Id { get; set; }
        [AutoExpand]
        public Customer Customer{ get; set; }
    }

    Result

    If you call return Product in response, Category will automatically expand and Customer will expand too. It works the same if you put [AutoExpand]on Class if you have more navigation properties to expand.

  • 4.16 Ignore query option

    In OData WebApi 5.7, we can ignore some query options when calling ODataQueryOption ApplyTo method, this is helpful when your odata service is integrate with other service that may already applied those query options.

    Customize

    public class MyEnableQueryAttribute : EnableQueryAttribute
    {
        public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
        {
           // Don't apply Skip and Top.
           var ignoreQueryOptions = AllowedQueryOptions.Skip | AllowedQueryOptions.Top;
           return queryOptions.ApplyTo(queryable, ignoreQueryOptions);
        }
    }

    Controller

    [MyEnableQuery]
    public IHttpActionResult Get()
    {
        return Ok(_products);
    }

    Result

    Then your queryOption won’t apply Top and Skip.

  • 4.17 Alternate keys

    Alternate keys is supported in Web API OData V5.7. For detail information about alternate keys, please refer to here

    The related sample codes can be found here

    Enable Alternate key

    Users can enable alternate key in the global configuration.

    HttpConfiguration config = ...
    config.EnableAlternateKeys(true);
    config.MapODataServiceRoute(...)

    Model builder

    So far, an Edm model with alternate keys can be built by ODL APIs.

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

    So, SSN is an alternate key.

    Routing for alternate key

    In OData controller, Users can use the attribute routing to route the alternate key. The Uri template is similiar to function parameter. For example:

    [HttpGet] 
    [ODataRoute("Customers(SSN={ssn})")] 
    public IHttpActionResult GetCustomerBySSN([FromODataUri]string ssn) 
    {
       ...
    }
  • 4.18 Add NextPageLink and $count for collection property

    In OData WebApi V5.7, it supports to add the NextPageLink and $count for collection property.

    It’s easy to enable the NextPageLink and $count for collection property in controller. Users can only put the [EnableQuery(PageSize=x)] on the action of the controller. For example:

    [EnableQuery(PageSize = 2)]  
    public IHttpActionResult GetColors(int key)  
    {  
      IList<Color> colors = new[] {Color.Blue, Color.Green, Color.Red};  
      return Ok(colors);
    }  

    Sample Requests & Response

    Request: GET http://localhost/Customers(5)/Colors?$count=true

    Response content:

    {  
      "@odata.context":"http://localhost/$metadata#Collection(NS.Color)",
      "@odata.count": 3,  
      "@odata.nextLink":"http://localhost/Customers(5)/Colors?$count=true&$skip=2",
      "value": [  
        ""Blue",  
        ""Green"  
      ]  
    } 
  • 4.19 Prefer odata.include-annotations

    Since OData WebApi V5.6, it supports odata.include-annotations.

    odata.include-annotations

    It supports the following four templates:

    1. odata.include-annotations=”*” // all annotations
    2. odata.include-annotations=”-*” // no annotations
    3. odata.include-annotations=”display.*” // only annotations under “display” namespace
    4. odata.include-annotations=”display.subject” // only annotation with term name “display.subject”

    Let’s have examples:

    odata.include-annotations=*

    We can use the following codes to request all annotations:

    HttpRequestMessage request = new HttpRequestMessage(...);
    request.Headers.Add("Prefer", "odata.include-annotations=*");
    HttpResponseMessage response = client.SendAsync(request).Result;
    ...

    The response will have all annotations:

    {  
      "@odata.context":"http://localhost:8081/$metadata#People/$entity",
      "@odata.id":"http://localhost:8081/People(2)",
      "Entry.GuidAnnotation@odata.type":"#Guid",
      "@Entry.GuidAnnotation":"a6e07eac-ad49-4bf7-a06e-203ff4d4b0d8",
      "@Hello.World":"Hello World.",
      "PerId":2,
      "Property.BirthdayAnnotation@odata.type":"#DateTimeOffset",
      "Age@Property.BirthdayAnnotation":"2010-01-02T00:00:00+08:00",
      "Age":10,
      "MyGuid":"f99080c0-2f9e-472e-8c72-1a8ecd9f902d",
      "Name":"Asha",
      "FavoriteColor":"Red, Green",
      "Order":{  
        "OrderAmount":235342,"OrderName":"FirstOrder"  
      }  
    }

    odata.include-annotations=Entry.*

    We can use the following codes to request specify annotations:

    HttpRequestMessage request = new HttpRequestMessage(...);
    request.Headers.Add("Prefer", "odata.include-annotations=Entry.*");
    HttpResponseMessage response = client.SendAsync(request).Result;
    ...

    The response will only have annotations in “Entry” namespace:

    {  
      "@odata.context":"http://localhost:8081/$metadata#People/$entity",
      "@odata.id":"http://localhost:8081/People(2)",
      "Entry.GuidAnnotation@odata.type":"#Guid",
      "@Entry.GuidAnnotation":"a6e07eac-ad49-4bf7-a06e-203ff4d4b0d8",
      "PerId":2,
      "Age":10,
      "MyGuid":"f99080c0-2f9e-472e-8c72-1a8ecd9f902d",
      "Name":"Asha",
      "FavoriteColor":"Red, Green",
      "Order":{  
        "OrderAmount":235342,"OrderName":"FirstOrder"  
      }  
    } 
  • 4.20 Prefer odata.continue-on-error

    Since OData Web API V5.7, it supports odata.continue-on-error.

    Enable odata.continue-on-error

    Users should call the following API to enable continue on error

    • For Microsoft.AspNet.OData (supporting classic ASP.NET Framework):
            var configuration = new HttpConfiguration();
            configuration.EnableContinueOnErrorHeader();
        
    • For Microsoft.AspNetCore.OData (supporting ASP.NET Core):

      It can be enabled in the service’s HTTP request pipeline configuration method Configure(IApplicationBuilder app, IHostingEnvironment env) of the typical Startup class:

            app.UseMvc(routeBuilder =>
            {
               routeBuilder.Select().Expand().Filter().OrderBy().MaxTop(100).Count()
                            .EnableContinueOnErrorHeader();  // Additional configuration to enable continue on error.
               routeBuilder.MapODataServiceRoute("ODataRoute", "odata", builder.GetEdmModel());
           });
        

    Prefer odata.continue-on-error

    We can use the following codes to prefer continue on error

    HttpRequestMessage request = new HttpRequestMessage(...);
    request.Headers.Add("Prefer", "odata.continue-on-error");
    request.Content = new StringContent(...);
    request.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0");
    HttpResponseMessage response = client.SendAsync(request).Result;
    ...

    The response will have all responses, includes the error responses.

  • 4.21 Set namespace for operations in model builder

    Since OData Web API V5.7, it allows to set a custom namespace for individual function and action in model builder.

    Set namespace for function and action in model builder

    ODataModelBuilder builder = new ODataModelBuilder();
    builder.Namespace = "Default";
    builder.ContainerName = "DefaultContainer";
    ActionConfiguration action = builder.Action("MyAction");
    action.Namespace = "MyNamespace";
    FunctionConfiguration function = builder.Function("MyFunction");
    function.Namespace = "MyNamespace";

    The setting works for ODataConventionModelBuilder as well.

  • 4.22 Use HttpRequestMessage Extension Methods

    In Microsoft.AspNet.OData, set of HttpRequestMessage extension methods are provided through HttpRequestMessageExtensions. For services that don’t use LINQ or ODataQueryOptions.ApplyTo(), those extension methods can offer lots of help.

    In Microsoft.AspNetCore.OData, which supports ASP.NET Core, set of HttpRequest extension methods are also provided through HttpRequestExtensions.

    They are pretty much symmetrical, and the differences will be noted in the examples below. For further details, please refer to Microsoft.AspNet.OData.Extensions.HttpRequestMessageExtensions and Microsoft.AspNet.OData.Extensions.HttpRequestExtensions.

    ODataProperties/IODataFeature

    OData methods and properties can be GET/SET through:

    • ODataProperties (for Microsoft.AspNet.OData) from httpRequestMessage.ODataProperties()
    • IODataFeature (for Microsoft.AspNetCore.OData) from httpRequest.ODataFeature()

    Each of them includes the followings:

    Path The ODataPath of the request.

    PathHandler Return DefaultODataPathHandler by default.

    RouteName The Route name for generating OData links.

    SelectExpandClause The parsed the OData SelectExpandClause of the request.

    NextLink Next page link of the results, can be set through GetNextPageLink.

    For example, we may need generate service root when querying ref link of a navigation property.

    private string GetServiceRootUri()
    {
      var routeName = Request.ODataProperties().RouteName;
      ODataRoute odataRoute = Configuration.Routes[routeName] as ODataRoute;
      var prefixName = odataRoute.RoutePrefix;
      var requestUri = Request.RequestUri.ToString();
      var serviceRootUri = requestUri.Substring(0, requestUri.IndexOf(prefixName) + prefixName.Length);
      return serviceRootUri;
    }

    GetModel

    For Microsoft.AspNet.OData only, get the EDM model associated with the request.

    IEdmModel model = this.Request.GetModel();

    Create a link for the next page of results, can be used as the value of @odata.nextLink. For example, the request Url is http://localhost/Customers/?$select=Name.

    Uri nextlink = this.Request.GetNextPageLink(pageSize:10);
    this.Request.ODataProperties().NextLink = nextlink;

    Then the nextlink generated is http://localhost/Customers/?$select=Name&$skip=10.

    GetETag

    Get the etag for the given request.

    EntityTagHeaderValue etagHeaderValue = this.Request.Headers.IfMatch.SingleOrDefault();
    ETag etag = this.Request.GetETag(etagHeaderValue);

    CreateErrorResponse

    For Microsoft.AspNet.OData only, create a HttpResponseMessage to represent an error.

    public HttpResponseMessage Post()
    {
      ODataError error = new ODataError()
      {
        ErrorCode = "36",
        Message = "Bad stuff",
        InnerError = new ODataInnerError()
        {
          Message = "Exception message"
        }
      };
    
      return this.Request.CreateErrorResponse(HttpStatusCode.BadRequest, error);
    }

    Then payload would be like:

    {
      "error":{
      "code":"36",
      "message":"Bad stuff",
      "innererror":{
        "message":"Exception message",
        "type":"",
        "stacktrace":""
      }
    }
    
  • 4.23 OData Simplified Uri convention

    OData v4 Web API 5.8 RC intruduces a new OData Simplefied Uri convention that supports key-as-segment and default OData Uri convention side-by-side.

    ~/odata/Customers/0
    ~/odata/Customers(0)

    To enable the ODataSimplified Uri convention, in WebApiConfig.cs:

    // ...
    var model = builder.GetEdmModel();
    
    // Set Uri convention to ODataSimplified
    config.SetUrlConventions(ODataUrlConventions.ODataSimplified);
    config.MapODataServiceRoute("odata", "odata", model);
  • 4.24 MaxExpansionDepth in EnableQueryAttribute

    Since Web API OData V5.9.1, it corrected the behavior of MaxExpansionDepth of EnableQueryAtrribute. MaxExpansionDepth means the max expansion depth for the $expand query option.

    When MaxExpansionDepth value is 0, it means the check is disabled, but if you use $level=max at the same time, the expand depth will be a default value : 2, to avoid the dead loop.

    Let’s see some samples about this behavior.

    $expand=Manager($levels=max) will be the same as $expand=Manager($expand=Manager)

    $expand=Manager($levels=3) will be the same as $expand=Manager($expand=Manager($expand=Manager))

    Related Issue #731.

  • 4.25 Bind Custom UriFunctions to CLR Methods

    Since Web API OData V5.9.0, it supports to bind the custom UriFunctions to CLR methods now, so user can add,modify or override the existing pre defined built-in functions.

    Let’s see how to use this feature.

    FunctionSignatureWithReturnType padrightStringEdmFunction =
                         new FunctionSignatureWithReturnType(
                        EdmCoreModel.Instance.GetString(true),
                        EdmCoreModel.Instance.GetString(true),
                        EdmCoreModel.Instance.GetInt32(false));
     
    MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) });
    const string padrightMethodName = "padright";
    ODataUriFunctions.AddCustomUriFunction(padrightMethodName, padrightStringEdmFunction, padRightStringMethodInfo);

    Then you can use filter function like $filter=padright(ProductName, 5) eq 'Abcd'.

    Related Issue #612.

  • 4.26 IN Operator

    IN Operator

    Starting in WebAPI OData V7.0.0 [ASP.NET ASP.NET Core], the IN operator is a supported feature that enables a shorthand way of writing multiple EQ expressions joined by OR. For example,

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

    can become

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

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

    Usage

    IN operator is supported only for $filter at the moment and hardcoded collections are supported with parentheses. See examples below.

    ~/Products?$filter=Name in ('Milk', 'Cheese')
    ~/Products?$filter=Name in RelevantProductNames
    ~/Products?$filter=ShipToAddress/CountryCode in MyShippers/Regions
    ~/Products?$filter=Name in Fully.Qualified.Namespace.MostPopularItemNames
    

5. SECURITY

  • 5.1 Basic authentication over HTTPS

    We’re often asked by people if OData APIs can be secured. The name “Open Data Protocol” and the way we evangelize it (by focusing on how open a protocol it is and how it provides interoperability) may give people the impression that OData APIs doesn’t work with authentication and authorization.

    The fact is that using OData is orthogonal to authentication and authorization. That is to say, you may secure an OData API in any way you can secure a generic RESTful API. We write this post to demonstrate it. The authentication methods we use in this post is the basic authentication over HTTPS. The service library we use is ASP.NET Web API for OData V4.0.

    Secure an OData Web API using basic authentication over HTTPS

    OData Protocol Version 4.0 has the following specification in section 12.1 Authentication:

    OData Services requiring authentication SHOULD consider supporting basic authentication as specified in [RFC2617] over HTTPS for the highest level of interoperability with generic clients. They MAY support other authentication methods.

    Supporting basic authentication over HTTPS is relatively easy for OData Web API. Suppose you already have a working OData service project. In this post, we implemented an OData API which has only one entity type Product and exposes only one entity set Products. In order to secure Products, the following steps needs to be taken:

    1. Create a custom AuthorizeAttribute for the basic authentication

    Add a class to your project as follows:

    public class HttpBasicAuthorizeAttribute : AuthorizeAttribute
    {
        public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
        {
            if (actionContext.Request.Headers.Authorization != null)
            {
                // get the Authorization header value from the request and base64 decode it
                string userInfo = Encoding.Default.GetString(Convert.FromBase64String(actionContext.Request.Headers.Authorization.Parameter));
    
                // custom authentication logic
                if (string.Equals(userInfo, string.Format("{0}:{1}", "Parry", "123456")))
                {
                    IsAuthorized(actionContext);
                }
                else
                {
                    HandleUnauthorizedRequest(actionContext);
                }
            }
            else
            {
                HandleUnauthorizedRequest(actionContext);
            }
        }
    
        protected override void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext)
        {
            actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized)
            {
                ReasonPhrase = "Unauthorized"
            };
        }
    }

    In this sample we name the attribute HttpBasicAuthorizeAttribute. It derives from System.Web.Http.AuthorizeAttribute. We override two of its methods: OnAuthorization and HandleUnauthorizedRequest.

    In OnAuthorization, we first get the base64-encoded value of the header Authorization and decode it. Then we apply our custom authentication logic to verify if the decoded value is a valid one. In this sample, we compare the decoded value to “Parry:123456”. As is specified in [RFC2617], this value indicates that the username is “Parry” and password is “123456”. In HandleUnauthorizedRequest, we handle unauthorized request by responding with HTTP status code 401 Unauthorized.

    2. Decorate the controller with the custom AuthorizeAttribute

    We decorate our ProductsController with HttpBasicAuthorizeAttribute:

    [HttpBasicAuthorize]
    public class ProductsController : ODataController
    {
    	
    }

    3. Enable HTTPS

    In the project properties window, enable the SSL and remember the SSL URL:

    4. Create a custom AuthorizationFilterAttribute for HTTPS

    Add a class to your project as follows:

    public class RequireHttpsAttribute : AuthorizationFilterAttribute
    {
        public override void OnAuthorization(HttpActionContext actionContext)
        {
            if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps)
            {
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                {
                    ReasonPhrase = "HTTPS Required"
                };
            }
            else
            {
                base.OnAuthorization(actionContext);
            }
        }
    }

    In this sample we name this class RequireHttpsAttribute. It derives from System.Web.Http.Filters.AuthorizationFilterAttribute and overrides its OnAuthorization method by responding with HTTP status code 403 HTTPS Required.

    5. Decorate the controller with the custom AuthorizationFilterAttribute

    We further decorate our ProductsController with RequireHttpsAttribute:

    [HttpBasicAuthorize]
    [RequireHttps]
    public class ProductsController : ODataController
    {
    	
    }

    6. Testing

    We run the project to test it. When run for the first time, you’ll be asked to create a self-signed certificate. Follow the instruction to create the certificate and proceed.

    In the above steps, we’ve secured the OData API by allowing only HTTPS connections to the Products and responding with data only to requests that has a correct Authorization header value (the base64-encoded value of “Parry:123456”: UGFycnk6MTIzNDU2). Our HTTP service endpoint is http://localhost:53277/ and our HTTPS endpoint is https://localhost:43300/.

    First of all, we send a GET request to http://localhost:53277/Products, and the service responds with an empty payload and the status code 403 HTTPS Required.

    Then we send the request over HTTPS to https://localhost:43300/Products. Since the basic authentication info needs to be provided. The service responds with an empty payload and the status code 401 Unauthorized.

    Finally, we set the value of the Authorization header to “Basic UGFycnk6MTIzNDU2” and send it over HTTPS to the same address again. The service now responds with the correct data.

    Summary

    In this post we demoed how an OData API can be secured by basic authentication over HTTPS. You may additionally add authorization logic to the API by further customizing the HttpBasicAuthorizeAttribute class we created. Furthermore, you may also use other authentication methods such as OAuth2 to secure your OData API. More information can be found at: http://www.asp.net/web-api/overview/security.

6. CUSTOMIZATION

  • 6.1 Custom URL parsing

    Let’s show how to extend the default OData Uri Parser behavior:

    Basic Case Insensitive Support

    User can configure as below to support basic case-insensitive parser behavior.

    HttpConfiguration config = 
    config.EnableCaseInsensitive(caseInsensitive: true);
    config.MapODataServiceRoute("odata", "odata", edmModel);

    Note: Case insensitive flag enables both for metadata and key-words, not only on path segment, but also on query option.

    For example:

    • ~/odata/$metaDaTa
    • ~/odata/cusTomers …

    Unqualified function/action call

    User can configure as below to support basic unqualified function/action call.

    HttpConfiguration config = 
    config.EnableUnqualifiedNameCall(unqualifiedNameCall: true);
    config.MapODataServiceRoute("odata", "odata", edmModel);

    For example:

    Original call:

    • ~/odata/Customers(112)/Default.GetOrdersCount(factor=1)

    Now, you can call as:

    • ~/odata/Customers(112)/GetOrdersCount(factor=1)

    Since Web API OData 5.6, it enables unqualified function in attribut routing. So, Users can add the unqualified function template. For example:

    [HttpGet]  
    ODataRoute("Customers({key})/GetOrdersCount(factor={factor})")]  
    public IHttpActionResult GetOrdersCount(int key, [FromODataUri]int factor)
    {
     ...
     }

    Enum prefix free

    User can configure as below to support basic string as enum parser behavior.

    HttpConfiguration config = 
    config.EnableEnumPrefixFree(enumPrefixFree: true);
    config.MapODataServiceRoute("odata", "odata", edmModel);

    For example:

    Origin call:

    * ~/odata/Customers/Default.GetCustomerByGender(gender=System.Web.OData.TestCommon.Models.Gender'Male')

    Now, you can call as:

    * ~/odata/Customers/Default.GetCustomerByGender(gender='Male')

    Advance Usage

    User can configure as below to support case insensitive & unqualified function call & Enum Prefix free:

    HttpConfiguration config = 
    config.EnableCaseInsensitive(caseInsensitive: true);
    config.EnableUnqualifiedNameCall(unqualifiedNameCall: true);
    config.EnableEnumPrefixFree(enumPrefixFree: true);
    
    config.MapODataServiceRoute("odata", "odata", edmModel);

    Thanks.

  • 6.2 Relax version constraints

    For both Web API OData V3 and V4, a flag IsRelaxedMatch is introduced to relax the version constraint. With IsRelaxedMatch = true, ODataVersionConstraint will allow OData request to contain both V3 and V4 max version headers (V3: MaxDataServiceVersion, V4: OData-MaxVersion). Otherwise, the service will return response with status code 400. The default value of IsRelaxdMatch is false.

    public class ODataVersionConstraint : IHttpRouteConstraint
    {
      ......
      public bool IsRelaxedMatch { get; set; }
      ......
    }

    To set this flag, API HasRelaxedODataVersionConstraint() under ODataRoute can be used as following:

    ODataRoute odataRoute = new ODataRoute(routePrefix: null, pathConstraint: null).HasRelaxedODataVersionConstraint();
  • 6.3 Customize OData Formatter

    This page illustrates how to use sensibility points in the ODataFormatter and plugin custom OData serializers/deserializers, gives a sample extends the ODataFormatter to add support of OData instance annotations.

    Let’s see this sample.

    CLR Model

    First of all, we create the following CLR classes as our model:

    public class Document
    {
            public Document()
            {
            }
            public Document(Document d)
            {
                ID = d.ID;
                Name = d.Name;
                Content = d.Content;
            }
            public int ID { get; set; }
            public string Name { get; set; }
            public string Content { get; set; }
            [NotMapped]
            public double Score { get; set; }
    }

    If I search for documents by sending the search query, in the result, I’d like have a score for the match for each document, as the score is dependent on the in coming query, it cannot be modeled as a property on response’s document, it should be modeled as an annotation on the document. Let’s do that.

    Build Edm Model

    Now, we can build a pretty simple Edm Model as:

    private static IEdmModel GetEdmModel()
    {
        ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<Document>("Documents");
        return builder.GetEdmModel();
    }

    Customize OData Formatter

    A custom entity serializer

    This entity serializer is to add the score annotation (org.northwind.search.score) to document entries.

    public class AnnotatingEntitySerializer : ODataEntityTypeSerializer
    {
        public AnnotatingEntitySerializer(ODataSerializerProvider serializerProvider)
               : base(serializerProvider)
        {
        }
        public override ODataEntry CreateEntry(SelectExpandNode selectExpandNode, EntityInstanceContext entityInstanceContext)
        {
            ODataEntry entry = base.CreateEntry(selectExpandNode, entityInstanceContext);
            Document document = entityInstanceContext.EntityInstance as Document;
            if (entry != null && document != null)
            {
                // annotate the document with the score.
                entry.InstanceAnnotations.Add(new                             ODataInstanceAnnotation("org.northwind.search.score", new ODataPrimitiveValue(document.Score)));
            }
            return entry;
        }
    }

    A custom serializer provider

    This serializer provider is to inject the AnnotationEntitySerializer.

    public class CustomODataSerializerProvider : DefaultODataSerializerProvider
    {
        private AnnotatingEntitySerializer _annotatingEntitySerializer;
        public CustomODataSerializerProvider()
        {
            _annotatingEntitySerializer = new AnnotatingEntitySerializer(this);
        }
        public override ODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType)
        {
            if (edmType.IsEntity())
            {
                return _annotatingEntitySerializer;
            }
            return base.GetEdmTypeSerializer(edmType);
        }
    }

    Setup configuration with customized ODataFormatter

    Create the formatters with the custom serializer provider and use them in the configuration.

    [HttpPost]
    public void Configuration(IAppBuilder appBuilder)
    {
        HttpConfiguration config = new HttpConfiguration();
        config.MapODataServiceRoute("odata", "odata", GetEdmModel());
        var odataFormatters = ODataMediaTypeFormatters.Create(new CustomODataSerializerProvider(), new DefaultODataDeserializerProvider());
        config.Formatters.InsertRange(0, odataFormatters);
        appBuilder.UseWebApi(config);
    }

    Controller Samples

    You should add a score to result documents.

    public IEnumerable<Document> GetDocuments(string search)
    {
        var results = FindDocument(search);
        return results.Select(d => new Document(d) { Score = ... });
    }

    Request Samples

    Add prefer header and send request.

    HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, baseAddress + "odata/Documents?search=test");
    request.Headers.TryAddWithoutValidation("Prefer", "odata.include-annotations=\"*\"");
    var response = client.SendAsync(request).Result;

    Response Samples

    Annotation is supported in newest night build, 5.6.0-beta1.

    {
        "@odata.context":"http://localhost:9000/odata/$metadata#Documents","value":
        [
            {
                "@org.northwind.search.score":1.0,"ID":1,"Name":"Another.txt","Content":"test"
            }
        ]
    }

    Conclusion

    The sample has a custom entity type serializer, AnnotatingEntitySerializer, that adds the instance annotation to ODataEntry by overriding the CreateEntry method. It defines a custom ODataSerializerProvider to provide AnnotatingEntitySerializer instead of ODataEntityTypeSerializer. Then creates the OData formatters using this serializer provider and uses those formatters in the configuration.

  • 6.4 Custom Stream Entity

    Since Web API OData V5.7, it supports to customize entity as stream.

    Fluent API

    Users can call fluent API to configure the stream entity. For example,

    var builder = new ODataModelBuilder();
    builder.EntityType<Vehicle>().MediaType();
    ...
    IEdmModel model = builder.GetEdmModel();

    Convention model builder

    Users can put [MediaType] attribute on a CLR class to configure the stream entity. For example,

    [MediaType]  
    public class Vehicle
    {
      [Key]
      public int Id {get;set;}
      ...
    }
    
    var builder = new ODataConventionModelBuilder();
    builder.EntityType<Vehicle>();
    IEdmModel modle = builder.GetEdmModel();
  • 6.5 Customize unsupported types

    ODataLib has a lot of its primitive types mapping to C# built-in types, for example, System.String maps to Edm.String, System.Guid maps to Edm.Guid. Web API OData adds supporting for some unsupported types in ODataLib, for example: unsigned int, unsigned long, etc.

    The mapping list for the unsupported types are:

7. RELEASE NOTES

  • 7.1 OData Web API 5.4 Beta

    The NuGet packages for OData Web API 5.4 beta are now available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API 5.4 beta using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData -Version 5.4.0-beta -Pre
    PM> Install-Package Microsoft.AspNet.WebApi.OData -Version 5.4.0-beta -Pre
    

    What’s in this release?

    This release primarily includes new features for OData (v4 and v3) Web API as summarized below:

    V4 package has a dependency on ODataLib 6.9.

    Questions and feedback

    You can submit questions related to this release, any issues you encounter and feature suggestions for future releases on our GitHub site.

  • 7.2 OData Web API 5.4 RC

    The NuGet packages for OData Web API 5.4 RC are now available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API 5.4 RC using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData -Version 5.4.0-rc -Pre
    PM> Install-Package Microsoft.AspNet.WebApi.OData -Version 5.4.0-rc -Pre
    

    What’s in this release?

    This release primarily includes new features for OData (v4 and v3) Web API as summarized below:

    V4 package has a dependency on ODataLib 6.9.

    Questions and feedback

    You can submit questions related to this release, any issues you encounter and feature suggestions for future releases on our GitHub site.

  • 7.3 OData Web API 5.4

    The NuGet packages for OData Web API 5.4 are now available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API 5.4 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData -Version 5.4.0
    PM> Install-Package Microsoft.AspNet.WebApi.OData -Version 5.4.0

    What’s in this release?

    This release primarily includes new features for OData (v4 and v3) Web API as summarized below:

    V4 package has a dependency on ODataLib 6.9.

    Questions and feedback

    You can submit questions related to this release, any issues you encounter and feature suggestions for future releases on our GitHub site.

  • 7.4 OData Web API 5.5

    The NuGet packages for OData Web API 5.5 are now available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API 5.5 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData -Version 5.5.0
    PM> Install-Package Microsoft.AspNet.WebApi.OData -Version 5.5.0

    What’s in this release?

    This release primarily includes new features for OData v4 Web API as summarized below:


    In this release, we moved the license from Microsoft OpenTech + Apache to Microsoft + MIT.

    v4 package has a dependency on ODataLib 6.10.

    Questions and feedback

    You can submit questions related to this release, any issues you encounter and feature suggestions for future releases on our GitHub site.

  • 7.5 OData Web API 5.5.1

    The NuGet packages for OData Web API 5.5.1 are now available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API 5.5.1 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData -Version 5.5.1
    PM> Install-Package Microsoft.AspNet.WebApi.OData -Version 5.5.1

    What’s in this release?

    • Fix the issue: $count segment doesn’t work in 5.5 with EF #290
  • 7.6 OData Web API 5.6 beta1

    The NuGet packages for OData Web API 5.6 beta1 are now available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API 5.6 beta1 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData -Version 5.6-beta1 -Pre
    

    What’s in this release?

    Bug fix

    #300: Function Date() doesn’t work with Linq To Entity.

  • 7.7 OData Web API 5.6

    The NuGet packages for OData Web API v5.6 are available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API v5.6 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData -Version 5.6.0
    

    What’s in this release?

    New Features:

    Bug Fixes:

    • GitHub Issue #300 : date() and time() function doesn’t work with EF.
    • GitHub Issue #317, Pull request #340 by OData team : Support nullable referential constraint with conventional model builder.
    • GitHub Issue #331, Pull request #336 by OData team : Support nullable enum prefix free in $filter.
    • GitHub Issue #281, Pull request #341 by OData team : OData serialization cannot serializer the derived complex types.
    • GitHub Issue #330, Pull request #350 by OData team : Dynamic property name is null use convention routing.
    • GitHub Issue #214, Pull request #343 by OData team : Issue about Web API model validation.
    • GitHub Issue #294, Pull request #323, #332 by OData team : Formatting <see>true</see>, <see langword="null"/> issue in xml comments about true, false, and null.

    OData Web API v5.6 package has a dependency on ODataLib 6.11.

    Questions and feedback

    You and your team are warmly welcomed to try out this new version if you are interested in the new features and fixes above. You are also welcomed to contribute your code to OData Web API repository. For any feature request, issue or idea please feel free to reach out to us at GitHub Issues.

  • 7.8 OData Web API 6.0.0 alpha1

    The NuGet packages for OData Web API v6.0.0 alpha1 are now available on the myget.

    Configure package source

    You can configure the NuGet package source for OData Web API v6.0.0 preview releases in the Package Manager Settings:

    Download this release

    You can install or update the NuGet packages for OData Web API v6.0.0 alpha1 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData -Version 6.0.0-alpha1 -Pre
    

    What’s in this release?

    This release contains the first preview of the next version of OData Web API which is built on ASP.NET 5 and MVC 6. This preview includes the basic support of:

    • Querying service metadata
    • Querying entity sets
    • CRUD of single entity
    • Querying structural or navigation property
    • $filter query option

    OData Web API v6.0.0 alpha1 package has a dependency on ODataLib 6.12.

    Where’s the sample service?

    You can take a look at a basic sample service built by this library.

    Now the sample service can support (but not limit to) the following requests:

    • Metadata. GET http://localhost:9091/odata/$metadata
    • EntitySet. GET http://localhost:9091/odata/Products
    • Entity. GET http://localhost:9091/odata/Products(1)
    • Structural property. GET http://localhost:9091/odata/Customers(1)/FirstName
    • Navigation property. GET http://localhost:9091/odata/Customers(1)/Products
    • $filter. GET http://localhost:9091/odata/Products?$filter=ProductId%20gt%201
    • Create. POST http://localhost:9091/odata/Products
    • Delete. DELETE http://localhost:9091/odata/Products(2)
    • Full update. PUT http://localhost:9091/odata/Products(2)

    Where’s the source code?

    You can view the source code of this library at our OData Web API repository. We warmly welcome any feedback, proposition and contribution from you!

  • 7.9 OData Web API 5.7 beta

    The NuGet packages for OData Web API v5.7 beta are available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API v5.7 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData -Version 5.7.0-beta -Pre
    

    What’s in this release?

    New Features:

    Bug Fixes:

    OData Web API v5.7-bata package has a dependency on ODataLib 6.13.

    Questions and feedback

    You and your team are warmly welcomed to try out this new version if you are interested in the new features and fixes above. You are also welcomed to contribute your code to OData Web API repository. For any feature request, issue or idea please feel free to reach out to us at GitHub Issues.

  • 7.10 OData Web API 5.7 rc

    The NuGet packages for OData Web API v5.7 rc are available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API v5.7 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData -Version 5.7.0-rc -Pre
    

    What’s in this release?

    New Features:

    Bug Fixes:

    OData Web API v5.7-rc package has a dependency on ODataLib 6.13.

    Questions and feedback

    You and your team are warmly welcomed to try out this new version if you are interested in the new features and fixes above. You are also welcomed to contribute your code to OData Web API repository. For any feature request, issue or idea please feel free to reach out to us at GitHub Issues.

  • 7.11 OData Web API 5.7

    The NuGet packages for OData Web API v5.7 are available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API v5.7 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData -Version 5.7.0
    

    What’s in this release?

    New Features:

    Bug Fixes:

    OData Web API v5.7 package has a dependency on ODataLib 6.13.

    Questions and feedback

    You and your team are warmly welcomed to try out this new version if you are interested in the new features and fixes above. You are also welcomed to contribute your code to OData Web API repository. For any feature request, issue or idea please feel free to reach out to us at GitHub Issues.

  • 7.12 OData Web API 5.8 beta

    The NuGet packages for OData Web API v5.8 beta are available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API v5.8 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData -Version 5.8.0-beta -Pre
    

    What’s in this release?

    Improvements and fixes:

    • Fixed typographical error, changed availabe to available in README. PR #519 by orthographic-pedant

    • [ConcurrencyCheck] attribute doesn’t work with EF. Issue #522, PR #529

    • Manually using ODataQueryOptions.Validate and setting SelectExpandQueryOption.LevelsMaxLiteralExpansionDepth. Issue #516, PR #524

    • CultureInfo property can’t be serialized. Issue #427, PR #542

    • Web API does not support Edm.Date literal in $filter when the property is Edm.Date [Nullable=True] and the backend is EF. Issue #482, PR #541

    • Add swagger model APIs. Issue #302, PR #520

    • Add operationId for Swagger json generation. Issue #302, PR #552

    • ETag can’t work for double type. Issue #475, PR #549

    • Expand query option contain $count don’t work. Issue #349, PR #553

    • EditLink is wrong in 5.7. Issue #543, PR #554

    • $count is evaluated prematurely at the call queryOptions.ApplyTo. Issue #1, PR #562

    • Unnecessary casts in expression when querying properties of the base class for the inheritor. Issue #560, PR #556 by Yuriy Soldatkin

    OData Web API v5.8-beta package has a dependency on ODataLib 6.13.

    Questions and feedback

    You and your team are warmly welcomed to try out this new version if you are interested in the new features and fixes above. You are also welcomed to contribute your code to OData Web API repository. For any feature request, issue or idea please feel free to reach out to us at GitHub Issues.

  • 7.13 OData Web API 5.8 rc

    The NuGet packages for OData v4 Web API 5.8 RC are available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API v5.8 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData -Pre
    

    What’s in this release?

    Improvements and fixes:

    • Fixed typographical error, changed availabe to available in README. PR #519 by orthographic-pedant

    • [ConcurrencyCheck] attribute doesn’t work with EF. Issue #522, PR #529

    • Manually using ODataQueryOptions.Validate and setting SelectExpandQueryOption.LevelsMaxLiteralExpansionDepth. Issue #516, PR #524

    • CultureInfo property can’t be serialized. Issue #427, PR #542

    • Add operationId for Swagger json generation. Issue #302, PR #552

    • ETag can’t work for double type. Issue #475, PR #549

    • EditLink is wrong in 5.7. Issue #543, PR #554

    • $count is evaluated prematurely at the call queryOptions.ApplyTo. Issue #1, PR #562

    • Unnecessary casts in expression when querying properties of the base class for the inheritor. Issue #560, PR #556 by Yuriy Soldatkin

    • Regression about the complex inheritance build. Issue #575, PR #577

    • ProcedureConfiguration API to support types known at runtime. PR #580 by Yogev Mizrahi

    • Array of enum doesn’t work for convention model builder. Issue #581, PR #582

    New Features:

    • Web API does not support Edm.Date literal in $filter when the property is Edm.Date [Nullable=True] and the backend is EF. Issue #482, PR #541

    • Add swagger model APIs. Issue #302, PR #520

    • Expand query option contain $count don’t work. Issue #349, PR #553

    • Added UrlConventions configuration in DefaultODataPathHandler that supports ODataSimplified Uri convention. PR #599 by Gan Quan

    • Make other nested query options in to work. Issue #557, PR #569

    • Added config options SerializeNullCollectionsAsEmpty and DoNotSerializeNullCollections. Issue #490, PR #583 by nickkin-msft

    • Enable to serialize the null dynamic propery by config. Issue #313, PR #573

    OData Web API v5.8 RC package has a dependency on OData v4 Lib 6.14.

    Questions and feedback

    You and your team are warmly welcomed to try out this new version if you are interested in the new features and fixes above. You are also welcomed to contribute your code to OData Web API repository. For any feature request, issue or idea please feel free to reach out to us at GitHub Issues.

  • 7.14 OData Web API 5.8

    The NuGet packages for OData v4 Web API 5.8 are available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API v5.8 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData
    

    What’s in this release?

    Improvements and fixes:

    • Fixed typographical error, changed availabe to available in README. PR #519 by orthographic-pedant

    • [ConcurrencyCheck] attribute doesn’t work with EF. Issue #522, PR #529

    • Manually using ODataQueryOptions.Validate and setting SelectExpandQueryOption.LevelsMaxLiteralExpansionDepth. Issue #516, PR #524

    • CultureInfo property can’t be serialized. Issue #427, PR #542

    • Add operationId for Swagger json generation. Issue #302, PR #552

    • ETag can’t work for double type. Issue #475, PR #549

    • EditLink is wrong in 5.7. Issue #543, PR #554

    • $count is evaluated prematurely at the call queryOptions.ApplyTo. Issue #1, PR #562

    • Unnecessary casts in expression when querying properties of the base class for the inheritor. Issue #560, PR #556 by Yuriy Soldatkin

    • Regression about the complex inheritance build. Issue #575, PR #577

    • ProcedureConfiguration API to support types known at runtime. PR #580 by Yogev Mizrahi

    • Array of enum doesn’t work for convention model builder. Issue #581, PR #582

    New Features:

    • Web API does not support Edm.Date literal in $filter when the property is Edm.Date [Nullable=True] and the backend is EF. Issue #482, PR #541

    • Add swagger model APIs. Issue #302, PR #520

    • Expand query option contain $count don’t work. Issue #349, PR #553

    • Added UrlConventions configuration in DefaultODataPathHandler that supports ODataSimplified Uri convention. PR #599 by Gan Quan

    • Make other nested query options in to work. Issue #557, PR #569

    • Added config options SerializeNullCollectionsAsEmpty and DoNotSerializeNullCollections. Issue #490, PR #583 by nickkin-msft

    • Enable to serialize the null dynamic propery by config. Issue #313, PR #573

    OData Web API v5.8 package has a dependency on OData v4 Lib 6.14.

    Questions and feedback

    You and your team are warmly welcomed to try out this new version if you are interested in the new features and fixes above. You are also welcomed to contribute your code to OData Web API repository. For any feature request, issue or idea please feel free to reach out to us at GitHub Issues.

  • 7.15 OData Web API 5.9

    The NuGet packages for OData v4 Web API 5.9 are available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API v5.9 beta using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData
    

    What’s in this release?

    Improvements and fixes:

    • Support Pass Null to EntitySet during Feed Serialization. Issue #617, PR #621

    • DataContractAttribute, etc don’t work for enum type. Issue #640

    • Provide an extensibility hook for consumers of ODataMediaTypeFormatter to customize base address of service root in OData uris. Issue #644, PR #645 by Jack Freelander

    • Using object key for null check in expression. Issue #559, PR #584 by Yuriy Soldatkin

    New Features:

    • Support PATCH to a complex type. Issue #135, PR #623

    • Added basic support for aggregations spec. Issue #70, PR #594 by Konstantin Kosinsky

    • Support Edm.Date. Issue #118, PR #600

    • Support “isof” query built-in function. Issue #185, PR #646

    • Advertise action/function in feed payload. Issue #637, PR #642

    • Bind Uri Functions to CLR methods. Issue #612, PR #613

    OData Web API v5.9 package has a dependency on OData v4 Lib 6.15.

    Questions and feedback

    You and your team are warmly welcomed to try out this new version if you are interested in the new features and fixes above. You are also welcomed to contribute your code to OData Web API repository. For any feature request, issue or idea please feel free to reach out to us at GitHub Issues.

  • 7.16 OData Web API 5.9.1

    The NuGet packages for OData v4 Web API 5.9.1 are available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API v5.9.1 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData
    

    What’s in this release?

    Improvements and fixes:

    • POST with GeographyPoint throws object must implement IConvertable. Issue #718

    • Model binding broken with enum keys. Issue #724

    • Update enum property doesn’t work. Issue #742

    • Delta<T> should avoid static properties. Issue #137

    • MaxExpansionDepth of 0 is not work. Issue #731

    • EnumMember support without value. Issue #697

    • Make IsIfNoneMatch public. Issue #764

    New Features:

    • ETagMessageHandler is not supporting typeless entities. Issue #172

    OData Web API v5.9.1 package has a dependency on OData v4 Lib 6.15.

    Questions and feedback

    You and your team are warmly welcomed to try out this new version if you are interested in the new features and fixes above. You are also welcomed to contribute your code to OData Web API repository. For any feature request, issue or idea please feel free to reach out to us at GitHub Issues.

  • 7.17 OData Web API 6.0.0-beta

    The NuGet packages for OData v4 Web API 6.0.0-beta are available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API v6.0.0-beta using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData -Pre
    

    What’s in this release?

    Breaking Changes:

    • Unify the entity and complex (collection) type serialization/deserialization, See [ odata issue #504 ]
      • Rename ODataFeed to ODataResourceSet
      • Rename ODataEntry to ODataResource
      • Rename ODataNavigationLink to ODataNestedResourceInfo
      • Rename ODataPayloadKind.Entry to ODataPayloadKind.Resource
      • Rename ODataPayloadKind.Feed to ODataPayloadKind.ResourceSet
      • Rename ODataEntityTypeSerializer to ODataResourceSerializer
      • Rename ODataFeedSerializer to ODataResourceSetSerizlier
      • Rename ODataEntityDeserializer to ODataResourceDeserializer
      • Rename ODataFeedDeserializer to ODataResourceSetDeserializer
      • Remove ODataComplexValue
      • Remove ODataComplexSerializer/ODataComplexTypeDeserializer
    • Issue #745 Support dependency injection (DI).
      • Integrate with the very popular DI framework Microsoft.Extensions.DependencyInjection.
      • Enable extremely easy customization of many services in Web API OData using DI.
      • Simplify APIs by removing redundant parameters and properties that have corresponding services registered in DI.
    • Issue #681 Using ODL path segment classes directly.
      • Remove all path segment classes defined in Web API OData.
      • Using the ODL path segment classes and template classes
    • Issue #693 Support new model bound attributes.
      • New attribute classes, ( for example FilterAttribute, OrderbyAttribute, etc ) used to enhance query options validation.
    • Support complex type with navigation property.
      • HasMany(), HasRequired(), HasOptional can be used on complex type to add navigation property.
      • Support navigation property on complex type in convention model builder.
      • Remove INavigationSourceConfiguration and DeclaringEntityType property
    • Support multiple navigation property bindings for a single navigation property by using different paths, see [ odata issue #629 ]
      • New BindingPathConfiguration<T> class used to add binding path
      • New NavigationPropertyBindingOption used to control binding in model builder.
    • Issue #764 public IsIfNoneMatch property

    • Issue #797 public Convert APIs in ODataModelBinderProvider.

    • Issue #172 ETagMessageHandler is not supporting typeless entities.

    • Issue #652 Some changes in Delta for complex/entity type delta.

    Migration ODL changes:

    • Simplified ODL namespaces, see [ odata issue #491 ]

    • Rename ODataUrlConvention to ODataUrlKeyDelimiter, see [ [odata issue #571] (https://github.com/OData/Odata.net/issues/571) ]
      • Rename ODataUrlConvention to ODataUrlKeyDelimiter.
      • Use ODataUrlKeyDelimiter.Slash instead of ODataUrlConvention.Simplified or ODataUrlConvention.KeyAsSegment
      • Use ODataUrlKeyDelimiter.Parentheses instead of ODataUrlConvention.Default
    • Change SerializationTypeNameAnnotation to ODataTypeAnnotation, see [ odata issue #614 ]

    • Change Enum member value type from IEdmPrimitiveValue to a more specific type, see [ odata issue #544 ]

    • Adjust query node kinds in Uri Parser in order to support navigation under complex. see [ odata issue #643 ]
      • Add SingleComplexNode and CollectionComplexNode to specifically represent complex type node.
      • Add SingleResourceNode as the base class of SingleEntityNode and SingleComplexNode, etc.
    • Rename CsdlXXX to SchemaXXX, and EdmxXXX to CsdlXXX, see [ odata issue #632 ]
      • CsdlReader/Writer to SchemaReader/Writer;
      • EdmxReader/Writer to CsdlReader/Writer;
      • EdmxReaderSettings to CsdlReaderSettings;
      • EdmxTarget to CsdlTarget
    • Remove Edm.ConcurrencyMode attribute. see [ odata issue #564 ]

    • Remove odata.null in ODL. It’s developer’s responsibility to check whether the return object is null or not.

    Improvements & Fixes:

    • Issue #719 ODataSwaggerConverter throws exception on composite key.

    • Issue #711 Using same enum twice in a model causes null reference exception.

    • Issue #697 EnumMember attribute without value.

    • Issue #726 Aggregation failed in Restier.

    • Issue #706 Change substringof to contained builtin function.

    • Issue #722 URI template parser doesn’t work correctly if key is of an enum type, see [ odata issue #556 ]

    • Issue #630 Orderby with duplicate properties.


    Here can find the OData V4 7.0.0 breaking changes docs and tutorials.

    Questions and feedback

    You and your team are warmly welcomed to try out this new version if you are interested in the new features and fixes above. You are also welcomed to contribute your code to OData Web API repository. For any feature request, issue or idea please feel free to reach out to us at GitHub Issues.

  • 7.18 OData Web API 6.0.0

    The NuGet packages for OData v4 Web API 6.0.0 are available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API v6.0.0 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData
    

    What’s in this release?

    Breaking Changes:

    • Unify the entity and complex (collection) type serialization/deserialization, See [ odata issue #504 ]
      • Rename ODataFeed to ODataResourceSet
      • Rename ODataEntry to ODataResource
      • Rename ODataNavigationLink to ODataNestedResourceInfo
      • Rename ODataPayloadKind.Entry to ODataPayloadKind.Resource
      • Rename ODataPayloadKind.Feed to ODataPayloadKind.ResourceSet
      • Rename ODataEntityTypeSerializer to ODataResourceSerializer
      • Rename ODataFeedSerializer to ODataResourceSetSerizlier
      • Rename ODataEntityDeserializer to ODataResourceDeserializer
      • Rename ODataFeedDeserializer to ODataResourceSetDeserializer
      • Remove ODataComplexValue
      • Remove ODataComplexSerializer/ODataComplexTypeDeserializer
    • Issue #745 Support dependency injection (DI).
      • Integrate with the very popular DI framework Microsoft.Extensions.DependencyInjection.
      • Enable extremely easy customization of many services in Web API OData using DI.
      • Simplify APIs by removing redundant parameters and properties that have corresponding services registered in DI.
    • Issue #681 Using ODL path segment classes directly.
      • Remove all path segment classes defined in Web API OData.
      • Using the ODL path segment classes and template classes
    • Issue #693 Support new model bound attributes.
      • New attribute classes, ( for example FilterAttribute, OrderbyAttribute, etc ) used to enhance query options validation.
      • Query options are disallowed by default, see detail in document.
    • Support complex type with navigation property.
      • HasMany(), HasRequired(), HasOptional can be used on complex type to add navigation property.
      • Support navigation property on complex type in convention model builder.
      • Remove INavigationSourceConfiguration and DeclaringEntityType property
    • Support multiple navigation property bindings for a single navigation property by using different paths, see [ odata issue #629 ]
      • New BindingPathConfiguration<T> class used to add binding path
      • New NavigationPropertyBindingOption used to control binding in model builder.
    • Issue #764 public IsIfNoneMatch property

    • Issue #797 public Convert APIs in ODataModelBinderProvider.

    • Issue #172 ETagMessageHandler is not supporting typeless entities.

    • Issue #652 Some changes in Delta for complex/entity type delta.

    Migration ODL changes:

    • Simplified ODL namespaces, see [ odata issue #491 ]

    • Rename ODataUrlConvention to ODataUrlKeyDelimiter, see [ [odata issue #571] (https://github.com/OData/Odata.net/issues/571) ]
      • Rename ODataUrlConvention to ODataUrlKeyDelimiter.
      • Use ODataUrlKeyDelimiter.Slash instead of ODataUrlConvention.Simplified or ODataUrlConvention.KeyAsSegment
      • Use ODataUrlKeyDelimiter.Parentheses instead of ODataUrlConvention.Default
    • Change SerializationTypeNameAnnotation to ODataTypeAnnotation, see [ odata issue #614 ]

    • Change Enum member value type from IEdmPrimitiveValue to a more specific type, see [ odata issue #544 ]

    • Adjust query node kinds in Uri Parser in order to support navigation under complex. see [ odata issue #643 ]
      • Add SingleComplexNode and CollectionComplexNode to specifically represent complex type node.
      • Add SingleResourceNode as the base class of SingleEntityNode and SingleComplexNode, etc.
    • Rename CsdlXXX to SchemaXXX, and EdmxXXX to CsdlXXX, see [ odata issue #632 ]
      • CsdlReader/Writer to SchemaReader/Writer;
      • EdmxReader/Writer to CsdlReader/Writer;
      • EdmxReaderSettings to CsdlReaderSettings;
      • EdmxTarget to CsdlTarget
    • Remove Edm.ConcurrencyMode attribute. see [ odata issue #564 ]

    • Remove odata.null in ODL. It’s developer’s responsibility to check whether the return object is null or not.

    Improvements & Fixes:


    Here can find the OData V4 7.0.0 breaking changes docs and tutorials.

    Questions and feedback

    You and your team are warmly welcomed to try out this new version if you are interested in the new features and fixes above. You are also welcomed to contribute your code to OData Web API repository. For any feature request, issue or idea please feel free to reach out to us at GitHub Issues.

  • 7.19 OData Web API 5.10

    The NuGet packages for OData v4 Web API 5.10 are available on the NuGet gallery.

    Download this release

    You can install or update the NuGet packages for OData Web API v5.10 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNet.OData
    

    What’s in this release?

    Improvements and fixes:

    • Delta feed should only serialize changed properties. Issue #857

    • Dynamic properties set to null should be written in a Delta Feed. Issue #927

    • @odata.Etag should be written for singletons, single-valued navigation properties #926

    New Features:

    • ODataProperties supports writing a delta link. Issue #900

    • A new EdmDeltaComplexObject is added to support serializing changed properties of a complex type. Issue #857

    • Added a new NavigationSource property for setting the entity set of related EdmDeltaEntityObjects and EdmDeltaDeletedEntityObjects in a flattened result. Issue #937

    OData Web API v5.10 package has a dependency on OData v4 Lib 6.15.

    Questions and feedback

    You and your team are warmly welcomed to try out this new version if you are interested in the new features and fixes above. You are also welcomed to contribute your code to OData Web API repository. For any feature request, issue or idea please feel free to reach out to us at GitHub Issues.

  • 7.20 OData Web API 6.1

    The NuGet packages for OData v4 Web API 6.1 are available on the NuGet gallery.

    What’s in this release?

    Improvements and fixes:

    • Problem with ODataResourceDeserializer’s ReadInline method. Issue #1005 -

    • ResourceContext.BuildResourceInstance can null ref when model and CLR names for a property do not match. Issue #990

    • String and Byte array values not handled properly in System.Web.OData. Issue #970

    • Fix ETags on Singletons. PR #951

    • Dynamic properties set to null should be written in a Delta Feed. Issue #927

    • Delta feed should only serialize changed properties. Issue #857

    • Parse failures with special characters in untyped data. Issue #938

    • Untyped data fix. PR #936

    • Fixes for writing Delta Responses - OData 60. PR #903

    • Bug in AssociationSetDiscoveryConvention for 6.0. Issue #892

    • Complex types in delta payload seem to be broken in 6.0. Issue #891

    • $apply used with Entity Framework causes memory leaks. Issue #874

    • Create a copy constructor for SelectExpandNode. PR #870

    New Features:

    • ODataProperties supports writing a delta link. Issue #900

    • A new EdmDeltaComplexObject is added to support serializing changed properties of a complex type. Issue #857

    • Added a new NavigationSource property for setting the entity set of related EdmDeltaEntityObjects and EdmDeltaDeletedEntityObjects in a flattened result. Issue #937

    • OData Web API v 5.4 does not support DateTime completely. Issue #221

  • 7.21 OData Web API 5.11

    The NuGet packages for OData v4 Web API 5.11 are available on the NuGet gallery.

    What’s in this release?

    Improvements and fixes:

    • Dynamic properties don’t have type. Relaxed null check in AggregateExpression and related changes in ApplyBinder to allow usage of dynamic properties in groupby clause. Pull Request #973

    • Working/fix apply enumerable.5x. Pull Request #971

    • String and Byte array values not handled properly in System.Web.OData. Issue #970

    • Fixes for writing Delta Responses - 5.x. Pull Request #901

    • Public request in ODataMediaTypeFormatter. Issue #737

    • the $skip and $top query options allow arithmetic overflows. Issue #578

    • Created virtual methods to override. Pull Request #547

    • Count with filter doesn’t work in ODATA queries. Issue #194

    New Features:

    • Adds support to SelectExpandBinder for etags on singletons and nav props. Pull Request #950

8. V1-3 SPECIFIC FEATURES

  • 8.1 ConcurrencyMode and ETag

    In OData V3 protocol, concurrencyMode is a special facet that can be applied to any primitive Entity Data Model (EDM) type. Possible values are None, which is the default, and Fixed. When used on an EntityType property, ConcurrencyMode specifies that the value of that declared property should be used for optimistic concurrency checks. In the metadata, concurrencyMode will be shown as following:

    <EntityType Name="Order">
        <Key>
            <PropertyRef Name="Id" />
        </Key>
        <Property Name="Id" Type="Edm.Int32" Nullable="false" />
        <Property Name="ConcurrencyCheckRow" Type="Edm.String" Nullable="false" ConcurrencyMode="Fixed" /> />  
    </EntityType>

    There are two approaches to set concurrencyMode for a primitive property: Using ConcurrencyCheck attribute:

    public class Order
    {
        public int Id { get; set; }
        
        [ConcurrencyCheck]
        public string ConcurrencyCheckRow { get; set; }
    }

    Using API call

    ODataModelBuilder builder = new ODataModelBuilder();
    builder.Entity<Order>().Property(c => c.ConcurrencyCheckRow).IsConcurrencyToken;

9. TEST

  • 9.1 Unit Test and E2E Test

    In OData WebApi, there are unit test, e2e test for V3 and V4, those test cases are to ensure the feature and bug fix, also to make sure not break old functionality.

    Unit Test

    Every class in OData WebApi has it’s own unit test class, for example: OData/src/System.Web.OData/OData/Builder/ActionLinkBuilder.cs ‘s test class is OData/test/UnitTest/System.Web.OData.Test/OData/Builder/ActionLinkBuilderTests.cs.

    You can find that the structural under System.Web.OData folder and System.Web.OData.Test folder are the same, also for V3 System.Web.Http.OData.Test, so if your pull request contains any class add/change, you should add/change(this change here means add test cases) unit test file.

    How To Add Unit Test

    • Try to avoid other dependency use moq.
    • Make sure you add/change the right class(V4 or V3 or both).
    • Can add functinal test for complicate scenario, but E2E test cases are better.

    E2E Test

    E2E test are complete test for user scenarios, always begin with client request and end with server response. If your unit test in pull request can’t cover all scenario well or you have a big pull request, please add E2E test for it.

    How To Add E2E Test

    • Add test cases in exist test class that related to your pull request.
    • Add new folder and test class for your own scenario.
    • If the test has any kind of state that is preserved between request, it should be the only test defined in the test class to avoid conflicts when executed along other tests.
    • Try to test with both in memory data and DB data.
    • Keep test folder, class style with exist test folder, class.

    Test Sample

    [NuwaFramework]
    public class MyTest
    {
    
        [NuwaBaseAddress]
        public string BaseAddress { get; set; }
    
        [NuwaHttpClient]
        public HttpClient Client { get; set; }
    
        [NuwaConfiguration]
        public static void UpdateConfiguration(HttpConfiguration config)
        {
            config.Routes.MapODataRoute("odata", "odata", GetModel());
        }     
    
        private static IEdmModel GetModel()
        {
            ODataModelBuilder builder = new ODataConventionModelBuilder();
            var customers = builder.EntitySet<Customer>("Customers");
            var orders = builder.EntitySet<Order>("Orders");
            return builder.GetEdmModel();
        }
    
        [Fact]
        public void GetCustomersWork()
        {
            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get,BaseAddress + "/odata/Customers");
            HttpResponseMessage response = Client.SendAsync(request).Result;
            Assert.Equal(HttpStatusCode.OK,response.StatusCode);
        }
    }
    
    [NuwaFramework]
    public class MyTest2
    {
        [NuwaBaseAddress]
        public string BaseAddress { get; set; }
    
        [NuwaHttpClient]
        public HttpClient Client { get; set; }
    
        [NuwaConfiguration]
        public static void UpdateConfiguration(HttpConfiguration config)
        {
            config.Routes.MapODataRoute("odata", "odata", GetModel());
        }
    
        private static IEdmModel GetModel()
        {
            ODataModelBuilder builder = new ODataConventionModelBuilder();
            var customers = builder.EntitySet<Customer>("Customers");
            var orders = builder.EntitySet<Order>("Orders");
            return builder.GetEdmModel();
        }
    
        [Fact]
        public void GetCustomersWork()
        {
            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, BaseAddress + "/odata/Customers");
            HttpResponseMessage response = Client.SendAsync(request).Result;
            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        }
    }
    
    public class CustomersController : ODataController
    {
        [Queryable(PageSize = 3)]
        public IHttpActionResult Get()
        {
            return Ok(Enumerable.Range(0, 10).Select(i => new Customer
            {
                Id = i,
                Name = "Name " + i
            }));
        }
    
        public IHttpActionResult Post(Customer customer)
        {
            return Created(customer);
        }
    }
    
    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public IList<Order> Orders { get; set; }
    }
    
    public class Order
    {
        public int Id { get; set; }
        public DateTime PurchaseDate { get; set; }
    }

10. OTHERS

  • 10.1 How To Debug

    If you want to debug OData Lib, WebAPI, Restier source, open DEBUG -> Options and Settings in VS, configure below things in General tab:

    1. Uncheck Enable Just My Code (Managed only).
    2. Uncheck Enable .NET Framework source stepping.
    3. One can find the source code for particular releases at https://github.com/OData/WebApi/tags. You can use these source files to properly step through your debugging session.
    4. Mark sure Enable Source Link support is checked.

    Setup your symbol source in Symbols tab:

    1. Check Microsoft Symbol Servers.
      • For versions of OData below 6.x, use the following
        • Add location: http://srv.symbolsource.org/pdb/Public (For preview/public releases in nuget.org).
        • Add location: http://srv.symbolsource.org/pdb/MyGet (For nightly build, and preview releases in myget.org).
      • For versions of OData 6.x and above, use the following
        • Add location: https://nuget.smbsrc.net/
        • To check for the existence of the symbols for your particular version, you can run the following command using NuGet.exe: nuget.exe list <namespace> -AllVersion -source https://nuget.smbsrc.net/. (Example: nuget.exe list Microsoft.AspNet.OData -AllVersion -source https://nuget.smbsrc.net/)
    2. Set the cache symbols directory in your, the path should be as short as it can be.

    Turn on the CLR first change exception to do a quick debug, open DEBUG -> Exceptions in VS, check the Common Language Runtime Exceptions.

  • 10.2 Work around for SingleResult.Create an empty result

    Note: This work around is for https://github.com/OData/WebApi/issues/170, which is not applicable for Microsoft.AspNetCore.OData v7.x.

    When SingleResult.Create takes in a query that returns an empty result, a SerializationException is being thrown.

    Let’s see a work-around about this issue.

    NullEntityTypeSerializer

    First of all, we define the NullEntityTypeSerializer to handle null value:

    public class NullEntityTypeSerializer : ODataEntityTypeSerializer
    {
        public NullEntityTypeSerializer(ODataSerializerProvider serializerProvider)
            : base(serializerProvider)
        { }
    
        public override void WriteObjectInline(object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext)
        {
            if (graph != null)
            {
                base.WriteObjectInline(graph, expectedType, writer, writeContext);
            }
        }
    }

    NullSerializerProvider

    Now, we can define a NullSerializerProvider, we need to avoid the situation of function,action call:

    public class NullSerializerProvider : DefaultODataSerializerProvider
    {
        private readonly NullEntityTypeSerializer _nullEntityTypeSerializer;
    
        public NullSerializerProvider()
        {
            _nullEntityTypeSerializer = new NullEntityTypeSerializer(this);
        }
    
        public override ODataSerializer GetODataPayloadSerializer(IEdmModel model, Type type, HttpRequestMessage request)
        {
            var serializer = base.GetODataPayloadSerializer(model, type, request);
            if (serializer == null)
            {
    			var functions = model.SchemaElements.Where(s => s.SchemaElementKind == EdmSchemaElementKind.Function
                                                                || s.SchemaElementKind == EdmSchemaElementKind.Action);
                var isFunctionCall = false;
                foreach (var f in functions)
                {
                    var fname = string.Format("{0}.{1}", f.Namespace, f.Name);
                    if (request.RequestUri.OriginalString.Contains(fname))
                    {
                        isFunctionCall = true;
                        break;
                    }
                }
    
                // only, if it is not a function call
                if (!isFunctionCall)
                {
                    var response = request.GetOwinContext().Response;
                    response.OnSendingHeaders(state =>
                    {
                        ((IOwinResponse)state).StatusCode = (int)HttpStatusCode.NotFound;
                    }, response);
                    
                    // in case you are NOT using Owin, uncomment the following and comment everything above
                    // HttpContext.Current.Response.StatusCode = (int)HttpStatusCode.NotFound;
                }
                return _nullEntityTypeSerializer;
            }
            return serializer;
        }
    }

    Formatters

    Add NullSerializerProvider in ODataMediaTypeFormatters:

    var odataFormatters = ODataMediaTypeFormatters.Create(new NullSerializerProvider(), new DefaultODataDeserializerProvider());
    config.Formatters.InsertRange(0, odataFormatters);

11. TOOLS

  • 11.1 OData V4 Web API Scaffolding

    Install Visual Studio Extension

    The installer of OData V4 Web API Scaffolding can be downloaded from Visual Studio Gallery: Microsoft Web API OData V4 Scaffolding. Double click vsix to install, the extension supports the VS2013 and VS2015, now.

    Generate Controller Code With Scaffolding

    The scaffolding is used to generate controller code for model class. Two kinds of scaffolders are provided: for model without entity framework(Microsoft OData v4 Web API Controller) and model using entity framework(Microsoft OData v4 Web API Controller Using Entity Framework).

    Scaffolder for model without entity framework:

    Before using scaffolding, you need to create a web api project and add model classes, the following is a sample:

    Then, you can right click “Controller” folder in solution explorer, select “Add” -> “Controller”. “Microsoft OData v4 Web API Controller” will be in the scaffolder list, as following:

    Select scaffoler item, then choose a model class you want to generate the controller. You can also select the “Using Async” if your data need to be got in Async call.

    After click “Add”, the controller will be genereted and added into your project. Meanwhile, all reference needed, including OData Lib and OData Web API, will be added into the project, too.

    Scaffolder for model using entity framework:

    If want to use entity framework as provider in service, no matter whether derived class of DbContext contained in project, when right click “Controller” folder in solution explorer, select “Add” -> “Controller” -> “Microsoft OData v4 Web API Controller Using Entity Framework” as scaffolder:

    Then you will see as following:

    Please select the existing Model (need build before scaffolding). You can select the existing data context class or add a new one:

    After click “Add”, the controller will be genereted and added into your project, new data context class will be added if needed. Meanwhile, all reference needed, including OData Lib and OData Web API, will be added into the project, too.

    Change WebApiConfig.cs File

    After generating the controller code, you may need to add some code in WebApiConfig.cs to generate model. Actually the code needed are in the comment of generated controller:

    Just need to copy/paste the code to WebApiConfig.cs.

    Add the Code to retrieve Data

    As Scaffolding only genreate the frameowrk of controller code, data retrieve part should also be added into controller generated. Here, we write a simple in-memory data source and return all of them when call “GetProducts” method:

    Add in ProductsController:

    private static List<Product> products = new List<Product>()
    {
      new Product() {Id = 1, Name = "Test1"},
    };
    

    Add in GetProducts Method:

    return Ok(products);
    

12. DESIGN

  • 12.1 Edm.Date and Edm.TimeOfDay with EF

    Problem

    The Transact-SQL has date (Format: YYYY-MM-DD) type, but there isn’t a CLR type representing date type. Entity Framework (EF) only supports to use System.DateTime CLR type to map the date type.

    OData V4 lib provides a CLR struct Date type and the corresponding primitive type kind Edm.Date. Web API OData V4 supports it. However, EF doesn’t recognize this CLR type, and it can’t map struct Date directly to date type.

    So, this doc describes the solution about how to support Edm.Date type with Entity Framework. Meanwhile, this doc also covers the Edm.TimeOfDay type with EF.

    Scopes

    It should support to map the type between date type in Database and Edm.Date type through the CLR System.DateTime type. The map is shown in the following figure:

    So, it should provide the below functionalities for the developer:

    1. Can configure the System.DateTime/System.TimeSpan property to Edm.Date/ Edm.TimeOfDay.
    2. Can serialize the date/ time value in the DB as Edm.Date /Edm.TimeOfDay value format.
    3. Can de-serialize the Edm.Date/Edm.TimeOfDay value as date/ time value into DB.
    4. Can do query option on the date/ time value.

    Most important, EF doesn’t support the primitive collection. So, Collection of date is not in the scope. The developer can use navigation property to work around.

    Detail Design

    Date & Time type in SQL DB

    Below is the date & time type mapping between DB and .NET:

    So, From .NET view, only System.DateTime is used to represent the date value, meanwhile only System.TimeSpan is used to represent the time value.

    Date & time mapping with EF

    In EF Code First, the developer can use two methodologies to map System.DateTime property to date column in DB:

    1 Data Annotation

    The users can use the Column Data Annotation to modify the data type of columns. For example:

      [Column(TypeName = "date")]
      public DateTime Birthday { get; set; }

    “date” is case-insensitive.

    2 Fluent API

    HasColumnName is the Fluent API used to specify a column data type for a property. For example:

    modelBuilder.EntityType<Customer>()
                .Property(c => c.Birthday)
                .HasColumnType("date");

    For time type, it implicitly maps the System.TimeSpan to represent the time value. However, you can use string literal “time” in DataAnnotation or fluent API explicitly.

    CLR Date Type in ODL

    OData Library defines one struct to hold the value of Edm.Date (Format: YYYY-MM-DD).

    namespace Microsoft.OData.Edm.Library
    {
        // Summary:
        //     Date type for Edm.Date
        public struct Date : IComparable, IComparable<Date>, IEquatable<Date>
       {
             
       }
    }

    Where, Edm.Date is the corresponding primitive type Kind.

    OData Library also defines one struct to hold the value of Edm.TimeOfDay (Format: HH:MM:SS. fractionalSeconds, where fractionalSeconds =1*12DIGIT).

    namespace Microsoft.OData.Edm.Library
    {
        // Summary:
        //     Date type for Edm.TimeOfDay 
        public struct TimeOfDay  : IComparable , IComparable<TimeOfDay>, IEquatable<TimeOfDay>
       {
             
       }
    }

    Where, Edm.TimeOfDay is the corresponding primitive type Kind.

    Configure Date & Time in Web API by Fluent API

    By default, Web API has the following mapping between CLR types and Edm types:

    We should provide a methodology to map System.DateTime to Edm.Date type, and System.TimeSpan to Edm.TimeOfDay type as follows:

    Extension methods

    We will add the following extension methods to re-configure System.DateTime & System.TimeSpan property:

    public static class PrimitivePropertyConfigurationExtensions 
    { 
      public static PrimitivePropertyConfiguration AsDate(this PrimitivePropertyConfiguration property)
      {}
    
      public static PrimitivePropertyConfiguration AsTimeOfDay(this PrimitivePropertyConfiguration property)
      {}
    } 

    For example, the developer can use the above extension methods as follows:

    public class Customer
    {
       
       public DateTime Birthday {get;set;}
       public TimeSpan CreatedTime {get;set;}
    }
    
    ODataModelBuilder builder = new ODataModelBuilder();
    EntityTypeConfiguration<Customer> customer = builder.EntityType<Customer>();
    customer.Property(c => c.Birthday).AsDate();
    customer.Property(c => c.CreatedTime).AsTimeOfDay();
    IEdmModel model = builder.GetEdmModel();

    Configure Date & Time in Web API by Data Annotation

    We should recognize the Column Data annotation. So, we will add a convention class as follows:

    internal class ColumnAttributeEdmPropertyConvention : AttributeEdmPropertyConvention<PropertyConfiguration>
    {
      
    }

    In this class, it will identify the Column attribute applied to System.DateTime or System.TimeSpan property, and call AsDate(…) or AsTimeOfDay() extension methods to add a Date or TimeOfDay mapped property. Be caution, EF supports the TypeName case-insensitive.

    After insert the instance of ColumnAttributeEdmPropertyConvention into the conventions in the convention model builder:

    For example, the developer can do as follows to build the Edm model:

    public class Customer
    {
      public int Id { get; set; }
          
      [Column(TypeName=date)]
      public DateTime Birthday { get; set; }
    
      [Column(TypeName=date)]
      public DateTime? PublishDay { get; set; }
    
      [Column(TypeName=time)]
      public TimeSpan CreatedTime { get; set; }
     }

    Now, the developer can call as follows to build the Edm model:

    ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
    
    builder.EntityType<Customer>();
    
    IEdmModel model = builder.GetEdmModel();

    Serialize

    System.DateTime value to Edm.Date

    We should modify ODataEntityTypeSerializer and ODataComplexTypeSerializer to identify whether or not the System.DataTime is serialized to Edm.Date. So, we should add a function in ODataPrimitiveSerializer:

    internal static object ConvertUnsupportedPrimitives(object value, IEdmPrimitiveTypeReference primitiveType)
    {
        Type type = value.GetType();
        if (primitiveType.IsDate() && TypeHelper.IsDateTime(type))
        {
             Date dt = (DateTime)value;
             return dt;
        }
         
    }
    System.TimeSpan value to Edm.TimeOfDay

    Add the following codes into the above function:

    if (primitiveType.IsTimeOfDay() && TypeHelper.IsTimeSpan(type))
    {
       TimeOfDay tod = (TimeSpan)value;
       return tod;
    }
    Top level property

    If the end user want to query the top level property, for example:

       ~/Customers(1)/Birthday

    The developer must take responsibility to convert the value into its corresponding type.

    De-serialize

    Edm.Date to System.DateTime value

    It’s easy to add the following code in EdmPrimitiveHelpers to convert struct Date to System.DateTime:

    if (value is Date)
    {
        Date dt = (Date)value;
        return (DateTime)dt;
    }
    Edm.TimeOfDay to System.TimeSpan value

    Add codes in EdmPrimitiveHelpers to convert struct TimeOfDay to System.TimeSpan:

    else if(type == typeof(TimeSpan))
    {
       if (value is TimeOfDay)
       {
           TimeOfDay tod = (TimeOfDay)value;
           return (TimeSpan)tod;
       }
    }

    Query options on Date & Time

    We should to support the following scenarios:

     ~/Customers?$filter=Birthday eq 2015-12-14
     ~/Customers?$filter=year(Birthday) ne 2015
     ~/Customers?$filter=Publishday eq null
     ~/Customers?$orderby=Birthday desc
     ~/Customers?$select=Birthday
     ~/Customers?$filter=CreatedTime eq 04:03:05.0790000"
    ...

    Fortunately, Web API supports the most scenarios already, however, we should make some codes changes in FilterBinder class to make TimeOfDay scenario to work.

    Example

    We re-use the Customer model in the Scope. We use the Lambda expression to build the Edm Model as:

    public IEdmModel GetEdmModel()
    {
      ODataModelBuilder builder = new ODataModelBuilder();
      var customer = builder.EntitySet<Customer>(Customers).EntityType;
      customer.HasKey(c => c.Id);
      customer.Property(c => c.Birthday).AsDate();
      customer.Property(c => c.PublishDay).AsDate();
      return builder.GetEdmModel();
    }

    Here’s the metadata document:

    <?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="NS" xmlns="http://docs.oasis-open.org/odata/ns/edm">
          <EntityType Name="Customer">
            <Key>
              <PropertyRef Name="Id" />
            </Key>
            <Property Name="Id" Type="Edm.Int32" Nullable="false" />
            <Property Name="Birthday" Type="Edm.Date" Nullable="false" />
            <Property Name="PublishDay" Type="Edm.Date" />
          </EntityType>
        </Schema>
        <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
          <EntityContainer Name="Container">
            <EntitySet Name="Customers" EntityType="NS.Customer " />
          </EntityContainer>
        </Schema>
      </edmx:DataServices>
    </edmx:Edmx>

    We can query:

    GET ~/Customers

    {
      "@odata.context": "http://localhost/odata/$metadata#Customers",
      "value": [
        {
          "Id": 1,
          "Birthday": "2015-12-31",
          "PublishDay": null
        },
        
      ]
    }

    We can do filter:

    ~/Customers?$filter=Birthday eq 2017-12-31

    {
      "@odata.context": "http://localhost/odata/$metadata#Customers",
      "value": [
        {
          "Id": 2,
          "Birthday": "2017-12-31",
          "PublishDay": null
        }
      ]
    }

    Thanks.

  • 12.2 WebApi 7.0 Default Setting Updates

    WebApi Default Enable Unqualified Operations and Case-insensitive Uri

    Overview

    OData Layer

    OData libraries 7.4.4+ contains updates to improve usability & compatibility of the library by virtue of exposing options that can be set by the caller of the OData core library (ODL). Related to request Uri parsing, the following two simplifications are now available when URI parser is configured properly:

    • As usual, namespace is the primary mechanism for resolving name token conflicts in multiple schema component of the model, therefore namespace is required up to OData v4. To improve flexibility, with notion of Default Namespaces introduced in OData v4.01, namespace qualifier is optional for function or action identifier in request Uri. When corresponding option in ODL Uri parser enabled:

      • If the function or action identifier contains a namespace qualifier, as in all the original cases, Uri parser uses original namespace-qualified semantic to ensure backward compatibility;

      • Otherwise, URI parser will search among the main schema and referenced sub-schemas treated as default namespaces, trying to resolve the unqualified function & action identifier to unique function / action element.

        • Exception will be thrown if no matches are found, or multiple functions or actions of same name are found in different namespaces of the model.

        • Property with same name as unqualified function / action name could cause the token being bound to a property segment unexpectedly. This should be avoided per best design practice in OData protocol: "Service designers should ensure uniqueness of schema children across all default namespaces, and should avoid naming bound functions, actions, or derived types with the same name as a structural or navigation property of the type."

    • In OData v4.01, case-insensitive name resolution is supported for system query options, built-in function and operator names, as well as type names, property names, enum values in form of strings. When corresponding option in ODL Uri parser is enabled, Uri parser first uses case-sensitive semantics as before and returns the result if exact match is found; otherwise, tries case-insensitive semantics, and returns the unique result found or throws exception for ambiguous result such as duplicated items.

      • Most of the case-insensitive support above has been implemented in current ODL version, except for minor bug fixes and case-insensitive support for built-in function, which are addressed as part of this task.

      Note the above options are also combinatorial, with expected behavior for Uri parsing.

      In ODL implementation, the primary support for the two options above is the default ODataUriResolver and its derived classes.

    WebApi Layer

    WebApi layer utilizes dependency injection to specify various services as options for URI parser. Dependencies can be specified in WebApi layer overriding default values provided by the IServicesProvider container.

    With the new default values, WebApi will exhibit different behavior related Uri parsing. The change should be backward compatible (all existing cases should work as it used to be), and previous error cases due to required case-sensitive and namespace qualifier for function should become working cases, hence improving usability of OData stack.

    Scenarios

    Write Scenario Description

    All scenarios are related to OData Uri parsing using default WebAPI settings. All sample scenarios assume no name collisions in EDM model, unless noted otherwise.

    Functions: namespace qualified/unqualified

    With function defined in the model: builder.EntityType<Customer>().Action("UpdateAddress");

    • Namespace qualified function should work as before:

    POST /service/Customers(1)/Default.UpdateAddress()

    • Namespace unqualified function should become a successful case:

    POST /service/Customers(1)/UpdateAddress()

    Case-Insensitive name resolution:

    • Case-insensitive property name should become resolvable:

      With model: public class InvalidQueryCustomer { public int Id { get; set; } }

      GET /service/InvalidQueryCustomers?$filter=id eq 5 : HTTP 200

      GET /service/InvalidQueryCustomers(5)?$filter=id eq 5 : HTTP 400 “Query options $filter, $orderby, $count, $skip, and $top can be applied only on collections.”

    • Case-insensitive customer uri function name should become resolvable:

      With model having entity type People defined and following customized Uri function:

      FunctionSignatureWithReturnType myFunc

      = new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetBoolean(true),

      EdmCoreModel.Instance.GetString(true), EdmCoreModel.Instance.GetString(true));

      // Add a custom uri function

      CustomUriFunctions.AddCustomUriFunction("myMixedCasestringfunction", myFunc);

      This should work:

      GET /service/People?$filter=mYMixedCasesTrInGfUnCtIoN(Name,'BlaBla') : HTTP 200

    • Combination of case-insensitive type & property name and unqualified function should become resolvable:

      With controller:

      [HttpGet]

      public ITestActionResult CalculateTotalOrders(int key, int month) {/*…*/}

      Following OData v4 Uris should work:

      GET /service/Customers(1)/Default. CalculateTotalOrders (month=1) : HTTP 200 GET /service/CuStOmErS(1)/CaLcUlAtEToTaLoRdErS (MONTH=1) : HTTP 200

    Design Strategy

    Dependency Injection of ODataUriResolver

    ODL (Microsoft.OData.Core) library supports dependency injection of a collection of service types from client via the IServiceProvider interface. The IServiceProvider can be considered as a container populated with default objects by ODL, while ODL’s client, such as WebApi, can override default objects by injecting customized dependencies.

    ODL IContainerBuilder and ContainerBuilderExtensions

    The ContainerBuilderExtensions.AddDefaultODataServices(this IContainerBuilder) implementation populates a collection of default OData service objects into the container’s builder. For example, default service of type ODataUriResolver is registered as one instance of ODataUriResolver as follows:

    public static IContainerBuilder AddDefaultODataServices(this IContainerBuilder builder)

    {

    //………

    builder.AddService(ServiceLifetime.Singleton, 

    sp => ODataUriResolver.GetUriResolver(null));

    //………

    }

    WebAPI dependency injection of customized ODataUriResolver:

    • WebAPI defines the DefaultContainerBuilder implementing the ODL’s IContainerBuilder interface.

    • When root container is created from HttpConfiguration (via the HttpConfigurationExtensions.CreateODataRootContainer), a PerRouteContainer instance will be used to:

      • Create an instance of DefaultContainerBuilder populated with default OData services noted in above;

      • Override the ODL’s default ODataUriResolver service instance in the container builder with WebApi’s new default for UnqualifiedODataUriResover with EnableCaseInsensitive=true.

        protected IContainerBuilder CreateContainerBuilderWithCoreServices()

        {

        //......
        

            builder.AddDefaultODataServices();

            // Set Uri resolver to by default enabling unqualified functions/actions and case insensitive match.

            builder.AddService(

                ServiceLifetime.Singleton,

                typeof(ODataUriResolver),

                sp => new UnqualifiedODataUriResolver {EnableCaseInsensitive = true});

            return builder;

        }

      • WebAPI client (per service) can further inject other dependencies (for example, typically, adding the EDM model) through the’configureAction’ argument of the following method from HttpConfigurationExtensions:

        internal static IServiceProvider CreateODataRootContainer(this HttpConfiguration configuration, string routeName, Action<IContainerBuilder> configureAction)

    ODataUriParser configuration with injected ODataUriResolver dependency

    When WebApi parses the request Uri, instance of ODataDefaultPathHandler is created with associated service provider container, which is further used to create ODataUriParser with injected dependency of ODataUriResolver.

    public ODataUriParser(IEdmModel model, Uri relativeUri, IServiceProvider container)
    

    Enable Case-Insensitive for Custom Uri function

    One issue is encountered when trying to bind function call token with case-insensitive enabled. The reason is that at the very beginning of the function BindAsUriFunction() the name token, when case-insensitive is enabled, is coerced to lower case (as shown below), which is valid for build-in function (such as ‘startswith’ and ‘geo.distance’, etc), but might not be valid for custom uri functions.

    private QueryNode BindAsUriFunction(FunctionCallToken functionCallToken, List<QueryNode> argumentNodes)

    {

        if (functionCallToken.Source != null)

        {

            // the parent must be null for a Uri function.

            throw new ODataException(ODataErrorStrings.FunctionCallBinder_UriFunctionMustHaveHaveNullParent(functionCallToken.Name));

        }

        string functionCallTokenName = this.state.Configuration.EnableCaseInsensitiveUriFunctionIdentifier ? functionCallToken.Name.ToLowerInvariant() : functionCallToken.Name;

    To implement with the correct behavior for enabled case-insensitive:

    • GetUriFunctionSignatures needs to additionally return signatures associated with function names in a dictionary instance.

    • When resolving best match function based on arguments for the invoked function, MatchSignatureToUriFunction will find the best match. Exception is still thrown in case of ambiguity or no matches found.

    Work Items

  • 12.3 Use $skiptoken for server side paging

    Use $skiptoken for server-driven paging

    Background

    Loading large data can be slow. Services often rely on pagination to load the data incrementally to improve the response times and the user experience. Paging can be server-driven or client-driven:

    Client-driven paging

    In client-driven paging, the client decides how many records it wants to load and asks the server that many records. That is achieved by using $skip and $top query options in conjunction. For instance, if a client needs to request 10 records from 71-80, it can send a similar request as below:

    GET ~/Products/$skip=70&$top=10

    Server-driven paging

    In server-driven paging, the client asks for a collection of entities and the server sends back partial results as well as a nextlink to use to retrieve more results. The nextlink is an opaque link which may use $skiptoken to store state about the request, such as the last read entity.

    Problem

    Currently, WebAPI uses $skip for server-driven paging which is a slight deviation from the OData standard and can be problematic when the data source can get updated concurrently. For instance, a deletion of a record may cause the last record to be sent down to the client twice.

    Proposed Solution

    WebAPI will now implement $skiptoken. When a collection of entity is requested which requires paging, we will assign the key value of the last sent entity to $skiptoken in the nextlink url prepended by values of the orderby properties in same order. While processing a request with $skiptoken, we will add another condition to the predicate based on the value of the skipoken.

    Technical details

    After all the query options have been applied, we determine if the results need pagination. If the results need pagination, we will pass the generated skiptoken value based off of the last result to the method that generates the nextpage link.

    The nextlink may contain $skiptoken if the result needs to be paginated. In WebAPI the $skiptoken value will be a list of pairs, where the pair consists of a property name and property value separated by a delimiter(:). The orderby property and value pairs will be followed by key property and value pairs in the value for $skiptoken. Each property and value pair will be comma separated.

    ~/Products?$skiptoken=Id:27
    ~/Books?$skiptoken=ISBN:978-2-121-87758-1,CopyNumber:11
    ~/Products?$skiptoken=Id:25&$top=40
    ~/Products?$orderby=Name&$skiptoken=Name:'KitKat',Id:25&$top=40
    ~/Cars(id)/Colors?$skip=4
    

    We will not use $skiptoken if the requested resource is not an entity type. Rather, normal skip will be used.

    This is the default format but services can define their own format for the $skiptoken as well but in that case, they will have to parse and generate the skiptoken value themselves.

    The next link generation method in GetNextPageHelper static class will take in the $skiptoken value along with other query parameters and generate the link by doing special handling for $skip, $skiptoken and $top. It will pass on the other query options as they were in the original request.

    1. Handle $skip

    We will omit the $skip value if the service is configured to support $skiptoken and a collection of entity is being requested. This is because the first response would have applied the $skip query option to the results already.

    2. Handle $top

    The next link will only appear if the page size is less than the $top query option. We will reduce the value of $top query option by the page size while generating the next link.

    3. Handle $skiptoken

    The value for the $skiptoken will be updated to new value passed in which is the key value for the last record sent. If the skiptoken value is not sent, we will call the existing method and use $skip for paging instead.

    Routing

    Since we will only be modifying the query options from the original request to generate the nextlink, the routing will remain same as the original request.

    Parsing $skiptoken and generating the Linq expression

    New classes will be created for SkipTokenQueryOption and SkipTokenQueryValidator. SkipTokenQueryOption will contain the methods to create and apply the LINQ expression based on the $skiptoken value. To give an example, for a query like the following:

    GET ~/EntitySet?$orderby=Prop1,Prop2&$skiptoken=Prop1:value1,Prop2:value2,Id1:idVal1,Id2:idVal2

    The following where clause will be added to the predicate:

    WHERE Prop1>value1
    Or (Prop1=value1 AND Prop2>value2)
    Or (Prop1=value1 AND Prop2=value2 AND Id1>Val)
    Or (Prop1=value1 AND Prop2=value2 AND Id1=idVal1 AND Id2>idVal2)
    

    Note that the greater than operator will be swapped for less than operator if the order is descending.

    Generating the $skiptoken

    The SkipTokenQueryOption class will be utilized by ODataQueryOption to pass the token value to the nextlink generator helper methods. In the process, IWebApiRequestMessage will be modified and GetNextPageLink method will be overloaded to now accept another parameter for the $skiptoken value.

    Configuration to use $skiptoken or $skip for server-driven paging

    We will allow services to configure if they want to use $skiptoken or $skip for paging per route as there can be performance issues with a large database with multipart keys. By default, we will use $skip.

    Moreover, we will ensure stable sorting if the query is configured for using $skiptoken.

    Additional details and discussions

    1. How would a developer implement paging without using EnableQuery attribute? What about stable ordering in that case?

    a. The new SkipTokenQueryOption class will provide 2 methods-

      i.	GenerateSkipTokenValue – requires the EDM model, the results as IQuerable and OrderbyQueryOption.
    
      ii.	ApplyTo -  applies the LINQ expression for $skiptoken.
      
      iii.  ParseSkipTokenValue - Populates the dictionary of property-value pairs on the class 
    

    For developers having non-linq data sources, they can generate the skiptoken value using the new class and use this class in their own implementation of the filtering that ApplyTo does.

    b. To ensure stable ordering, we will provide a public method on ODataQueryOptions - GenerateStableOrderQueryOption: It will output an OrderbyQueryOption which can be passed to the skiptoken generator.

    Developers not using the EnableQuery attribute will have to generate their own OrderbyQueryOption and generate the skiptoken value themselves.

    Currently, the way the code is structured, a lot of the information about the current query ($apply and $orderby) would need to be passed down to the nextlink generator to append to the orderby and moreover, it will make it very cumbersome for developers not using the enable query attribute to use it.

    Instead, we will expose methods on ODataQueryOption that will enable developers to generate their orderby clauses for stable sorting.

    Currently, the developers not using the enable query attribute generate the next link by using GetNextPageLink extension method on the request. Considering that the data source can even be linq incompatible, this will be a significant deviation from the current implementation for such developers. Moreover, the need to filter the results based on a certain value fits more into the QueryOption paradigm and makes it more suitable for customers supporting linq.

13. 6.X FEATURES

  • 13.1 Model Bound Attributes

    Since Web API OData V6.0.0 which depends on OData Lib 7.0.0, we add a new feature named ModelBoundAttribute, use this feature, we can control the query setting through those attributes to make our service more secure and even control the query result by set page size, automatic select, automatic expand.

    Let’s see how to use this feature.

    Global Query Setting

    Now the default setting for WebAPI OData is : client can’t apply $count, $orderby, $select, $top, $expand, $filter in the query, query like localhost\odata\Customers?$orderby=Name will failed as BadRequest, because all properties are not sort-able by default, this is a breaking change in 6.0.0, if we want to use the default behavior that all query option are enabled in 5.x version, we can configure the HttpConfigration to enable the query option we want like this:

    //...
    configuration.Count().Filter().OrderBy().Expand().Select().MaxTop(null);
    configuration.MapODataServiceRoute("odata", "odata", edmModel);
    //...

    Page Attribute

    Pagination settings correlate to OData’s @odata.nextLink (server-side pagination) and ?$top=5&$skip=5 (client-side pagination). We can set the PageSize to control the server-side pagination, and MaxTop to control the maximum value for $top, by default client can’t use $top as we said in the Global Query Setting section, every query option is disabled, if you set the Page Attribute, by default it will enable the $top with no-limit maximum value, or you can set the MaxTop like:

    [Page(MaxTop = 5, PageSize = 1)]
    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Order Order { get; set; }
        public Address Address { get; set; }
        [Page(MaxTop = 2, PageSize = 1)]
        public List<Order> Orders { get; set; }
        public List<Address> Addresses { get; set; }
    }
    
    public class Order
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Price { get; set; }
        [Page]
        public List<Customer> Customers { get; set; }
    }

    In the model above, we defined the page setting for Customer and Orders navigation property in Customer and Customers navigation property in Order, let’s explain the usage of them one by one.

    Page Attribute on Entity Type

    The first page attribute on Customer type, means the query setting when we query the Customer type, like localhost\odata\Customers, the max value for $top is 5 and page size of returned customers is 1.

    For example:

    The query like localhost\odata\Customers?$top=10 will failed with BadRequest : The limit of ‘5’ for Top query has been exceeded.

    The page size is 1 if you request localhost\odata\Customers.

    Page Attribute on Navigation Property

    And what about the page attribute in Order type’s navigation property Customers? it means the query setting when we query the Customers navigation property in Order type. Now we get a query setting for Customer type and a query setting for Customers navigation property in Order type, how do we merge these two settings? The answer is: currently the property’s query setting always override the type’s query setting, if there is no query setting on property, it will inherent query setting from it’s type.

    For example:

    The query like localhost\odata\Orders?$expand=Customers($top=10) will works because the setting is no limit.

    The result of localhost\odata\Orders?$expand=Customers won’t have paging because the setting didn’t set the page size.

    So for the attribute on Orders navigation property in Customer type, the page size and maximum value of $top setting will have effect when we request like localhost\odata\Customers?$expand=Orders or localhost\odata\Customers(1)\Orders as long as we are query the Orders property on Customer type.

    Count Attribute

    Count settings correlate to OData’s ?$count=true (items + count). We can set the entity type or property is countable or not like:

    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Address Address { get; set; }
        [Count]
        public List<Order> Orders { get; set; }
        public List<Address> Addresses { get; set; }
        public List<Address2> Addresses2 { get; set; }
        public List<Order> CountableOrders { get; set; }
    }
    
    [Count(Disabled = true)]
    public class Order
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Price { get; set; }
    }

    In the model above, we can tell that the Order is not countable(maybe the number is very large) but Orders property in Customer is countable.

    About the priority of attribute on property and type, please refer to Page Attribute section.

    So you can have those examples:

    Query localhost\odata\Orders?$count=true will failed with BadRequest that orders can’t used for $count

    Query localhost\odata\Customers?$expand=Orders($count=true) will works

    Query localhost\odata\Customers(1)/Orders?$count=true works too.

    OrderBy Attribute

    Ordering settings correlate to OData’s $orderby query option. We can specify which property is sort-able very easy and we can also define very complex rule by use attribute on property and on type. For example:

    [OrderBy("AutoExpandOrder", "Address")]
    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Order AutoExpandOrder { get; set; }
        [OrderBy]
        public Address Address { get; set; }
        [OrderBy("Id")]
        public List<Order> Orders { get; set; }
    }
        
    [OrderBy("Name", Disabled = true)]
    [OrderBy]
    public class Order
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Price { get; set; }   
        [OrderBy]
        public List<Customer> Customers { get; set; }
        public List<Customer> UnSortableCustomers { get; set; }
        public List<Car> Cars { get; set; }
    }
    
    public class Address
    {
        public string Name { get; set; }
        public string Street { get; set; }
    }

    Multiple Attribute

    We can see that the we can have multiple OrderBy attributes, how are they merged? The answer is the Attribute on a class with a constrained set of properties gets high priority, the order of their appear time doesn’t matter.

    OrderBy Attribute on EntityType and ComplexType

    Let’s go through those attributes to understand the settings, the first attribute means we can specify the single navigation property AutoExpandOrder and single complex property Address when we query Customer type, like query localhost\odata\Customers?$orderby=Address/xxx or localhost\odata\Customers?$orderby=AutoExpandOrder/xxx. And how do we control which property under AutoExandOrder is sort-able?

    For the AutoExpandOrder property, we add OrderBy Attribute on Order type, the first attribute means Name is not sort-able, the second attribute means all the property is sort-able, so for the Order type, properties except Name are sort-able.

    For example:

    Query localhost\odata\Customers?$orderby=Name will failed with BadRequest that Name is not sort-able.

    Query localhost\odata\Customers?$orderby=AutoExpandOrder/Price works.

    Query localhost\odata\Customers?$orderby=AutoExpandOrder/Name will failed with BadRequest that Name is not sort-able.

    OrderBy Attribute on Property

    About the priority of attribute on property and type, please refer to Page Attribute section. We have OrderBy attribute on Address property, it means all properties are sort-able when we query Customer, and for Orders property, it means only Id is sort-able when we query Orders property under Customer.

    For example:

    Query localhost\odata\Customers?$orderby=Address/Name works.

    Query localhost\odata\Customers?$expand=Orders($orderby=Id) works.

    Query localhost\odata\Customers?$expand=Orders($orderby=Price) will failed with BadRequest that Price is not sort-able.

    Filter Attribute

    Filtering settings correlate to OData’s $filter query option. For now we only support to specify which property can be filter just like what we do in OrderBy Attribute, we can simply replace orderby with filter in the example above, so please refer to OrderBy Attribute section.

    Select Attribute

    Search settings correlate to OData’s $search query option. We can specify which property can be selected, which property is automatic selected when there is no $select in the query.

    Automatic Select

    Automatic select mean we will add $select in the query depends on the select attribute.

    If we have a User class, and we don’t want to expose some property to client, like secrete property, so client query localhost\odata\Users?$select=Secrete will failed and query localhost\odata\Users? won’t return Secrete property, how can we achieve that with Select Attribute?

    [Select(SelectType = SelectExpandType.Automatic)]
    [Select("Secrete", SelectType = SelectExpandType.Disabled)]
    public class User
    {
        public int Id { get; set; }
        public string Secrete { get; set; }
        public string Name { get; set; }
    }

    The first attribute means all the property will be automatic select when there is no $select in the query, the second attribute means the property Secrete is not select-able. For example, request localhost\odata\Users will have the same response with localhost\odata\Users?$select=Id,Name

    Automatic Select on Derived Type

    If the target type of our request have some derived types which have automatic select property, then these property will show in the response if there is no $select query option, for example, request localhost\odata\Users will have the same response with localhost\odata\Users?$select=Id,Name,SpecialUser/SpecialName if the SpecinalName property in automatic select.

    Select Attribute on Navigation Property

    About the priority of attribute on property and type, please refer to Page Attribute section. About the multiple attribute, please refer to Multiple Attribute section. We also support Select attribute on navigation property, to control the expand scenario and property access scenario, like if we want client can only select Id and Name from Customer’s navigation property Order.

    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        [Select("Id", "Name")]
        public Order Order { get; set; }
    }

    Expand Attribute

    Expansion settings correlate to OData’s $expand query option. We can specify which property can be expanded, which property is automatic expanded and we can specify the max depth of the expand property. Currently we support Expand attribute on entity type and navigation property, the using scenario is quite like Select Attribute and other attributes, you can just refer to those sections.

    Automatic Expand

    Automatic expand mean it will always expand that navigation property, it’s like automatic select, we will add a $expand in the query, so it will expand even if there is a $select which does not contain automatic epxand property.

    Model Bound Fluent APIs

    We also provide all fluent APIs to configure above attributes if you can’t modify the class by adding attributes, it’s very straight forward and simple to use:

    [Expand("Orders", "Friend", "CountableOrders", MaxDepth = 10)]
    [Expand("AutoExpandOrder", ExpandType = SelectExpandType.Automatic, MaxDepth = 8)]
    [Page(MaxTop = 5, PageSize = 1)]
    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        [Expand(ExpandType = SelectExpandType.Disabled)]
        public Order Order { get; set; }
        public Order AutoExpandOrder { get; set; }
        public Address Address { get; set; }
        [Expand("Customers", MaxDepth = 2)]
        [Count(Disabled = true)]
        [Page(MaxTop = 2, PageSize = 1)]
        public List<Order> Orders { get; set; }
        public List<Order> CountableOrders { get; set; }
        public List<Order> NoExpandOrders { get; set; }
        public List<Address> Addresses { get; set; }
        [Expand(MaxDepth = 2)]
        public Customer Friend { get; set; }
    }
    
    var builder = new ODataConventionModelBuilder();
    builder.EntitySet<Customer>("Customers")
        .EntityType.Expand(10, "Orders", "Friend", "CountableOrders")
        .Expand(8, SelectExpandType.Automatic, "AutoExpandOrder")
        .Page(5, 2);
    builder.EntityType<Customer>()
        .HasMany(p => p.Orders)
        .Expand(2, "Customers")
        .Page(2, 1)
        .Count(QueryOptionSetting.Disabled);
    builder.EntityType<Customer>()
        .HasMany(p => p.CountableOrders)
        .Count();
    builder.EntityType<Customer>()
        .HasOptional(p => p.Order)
        .Expand(SelectExpandType.Disabled);

    The example shows class with attributes and build model using the model bound fluent APIs if we can’t modify the class. These two approaches are getting two same models. About the multiple attribute, model bound fluent APIs are the same, the model bound fluent API with a constrained set of properties wins. For example: builder.EntityType<Customer>().Expand().Expand("Friend", SelectExpandType.Disabled), Friend can’t be expanded, even we put Expand() in the end. If there is a setting with same property, the last one wins, for example: .Expand(8, "Friend").Expand(1, "Friend"), the max depth will be 1.

    Overall Query Setting Precedence

    Query settings can be placed in many places, with the following precedence from lowest to highest: System Default(not query-able by default), Global Configuration, Model Bound Attribute, Fluent API.

    Controller Level Query Setting

    If we only want to control the setting in one API call, like the Get() method in CustomerController, we can simply use the Settings in EnableQueryAttribute, like:

    [EnableQuery(MaxExpansionDepth = 10)]
    public List<Customer> Get()
    {
        return _customers;
    }

    The model bound attribute and the settings in EnableQueryAttribute are working separately, so the query violent one of them will fail the request.

    More test case with more complex scenario can be find at here.

    You can participate into discussions and ask questions about this feature at our GitHub issues, your feedback is very important for us.

  • 13.2 Complex Type with Navigation Property

    Since Web API OData V6.0.0 beta, It supports to configure navigation property on complex type.

    Let’s have an example to illustrate how to configure navigation property on complex type:

    CLR Model

    We use the following CLR classes as the CLR model:

    public class Address
    {
      public City CityInfo { get; set; }
      public IList<City> Cities { get; set}
    }
    
    public class City
    {
      public int Id { get; set; }
    }

    Where:

    • Address is a complex type.
    • City is an entity type.

    Add navigation properties

    The following APIs are used to add navigation properties for complex type:

    1. HasMany()
    2. HasRequired()
    3. HasOptional()

    So, we can do as:

    ODataModelBuilder builder = new ODataModelBuilder();
    builder.EntityType<City>().HasKey(c => c.Id);
    var address = builder.ComplexType<Address>();
    address.HasRequired(a => a.CityInfo);
    address.HasMany(a => a.Cities);

    We can get the following result:

    <ComplexType Name="Address">
      <NavigationProperty Name="CityInfo" Type="ModelLibrary.City" Nullable="false" />"
      <NavigationProperty Name="Cities" Type="Collection(ModelLibrary.City)" />"
    </ComplexType>
    <EntityType Name="City">
      <Key>
        <PropertyRef Name="Id" />
      </Key>
      <Property Name="Id" Type="Edm.Int32" Nullable="false" />
    </EntityType>
    

    Add navigation properties in convention model builder

    Convention model builder will automatically map the class type properties in complex type as navigation properties if the declaring type of such navigation property has key defined.

    So, as the above example, we can use the following codes to define a convention model:

    ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
    builder.ComplexType<Address>(); // just add a starting point

    As result, We can get the following result:

    
    <edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
      <edmx:DataServices>
        <Schema Namespace="ModelLibrary" xmlns="http://docs.oasis-open.org/odata/ns/edm">
          <ComplexType Name="Address">
            <NavigationProperty Name="CityInfo" Type="ModelLibrary.City" />
            <NavigationProperty Name="Cities" Type="Collection(ModelLibrary.City)" />
          </ComplexType>
          <EntityType Name="City">
            <Key>
              <PropertyRef Name="Id" />
            </Key>
            <Property Name="Id" Type="Edm.Int32" Nullable="false" />
          </EntityType>
        </Schema>
        <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
          <EntityContainer Name="Container" />
        </Schema>
      </edmx:DataServices>
    </edmx:Edmx>
    
    
  • 13.3 Multiple Navigation property binding path

    Since Web API OData V6.0.0 beta, It supports to configure multiple navigation property binding paths.

    Original navigation property binding

    The following APIs are used to add navigation property binding in model builder.

    1. HasManyBinding()
    2. HasRequiredBinding()
    3. HasOptionalBinding()
    

    So, we can do as:

    public class Customer
    {
      public int Id { get; set; }
      
      public Order SingleOrder {get;set;}  // single navigation property
      
      public IList<Order> Orders {get;set;} // collection navigation property
    }
    
    public class Order
    {
      public int Id { get; set; }
    }
    
    ODataModelBuilder builder = new ODataModelBuilder();
    var customers = builder.EntitySet<Cusomter>("Customers");
    customers.HasManyBinding(c => c.Orders, "Orders");
    customers.HasRequiredBinding(c => c.SingleOrder, "SingleOrders");

    We can get the following result:

    <EntityContainer Name="Container">
      <EntitySet Name="Customers" EntityType="System.Web.OData.Builder.Cusomter">
        <NavigationPropertyBinding Path="Orders" Target="Orders" />
        <NavigationPropertyBinding Path="SingleOrder" Target="SingleOrders" />
      </EntitySet>
      <EntitySet Name="Orders" EntityType="System.Web.OData.Builder.Order" />
      <EntitySet Name="SingleOrders" EntityType="System.Web.OData.Builder.Order" />
    </EntityContainer>
    

    Where, the binding path is straight-forward, just as the navigation property name. Before 6.0.0, it doesn’t support to:

    1. Add the complex property into binding path
    2. Add the type cast into binding path

    Multiple navigation property binding path in model builder

    In Web API OData V6.0.0 beta, we add a new generic type class to configure the multiple binding path:

    BindingPathConfiguration{TStructuralType}

    In this class, it provides the following APIs to add binding path:

    1. HasManyPath()
    2. HasSinglePath()

    It also provides the following APIs to bind the navigation property with multiple binding path to a targe navigation source.

    1. HasManyBinding()
    2. HasRequiredBinding()
    3. HasOptionalBinding()

    So, the normal navigation property binding configuration flow is:

    1. Call Binding from NavigationSourceConfiguration{TEntityType} to get an instance of BindingPathConfiguration{TEntityType}
    2. Call HasManyPath()/HasSinglePath from BindingPathConfiguration{TEntityType}` to add a binding path
    3. Repeat step-2 if necessary
    4. Call HasManyBinding()/HasRequiredBinding()/HasOptionalBinding() to add the target navigation source.

    Here’s an example:

    builder.EntitySet<Customer>("Customers")
       .Binding
       .HasManyPath(c => c.Locations)
       .HasSinglePath(a => a.LocationInfo)
       .HasRequiredBinding(c => c.City, "Cities");
       

    Example

    Let’s have the following CLR classes as the model:

    Entity types

    public class Cusomter
    {
      public int Id { get; set; }
    
      public Animal Pet { get; set; }
    }
    
    public class VipCustomer : Cusomter
    {
      public List<Address> VipLocations { get; set; }
    }
    
    public class City
    {
      public int Id { get; set; }
    }

    Complex types

    public class Animal
    {
    }
    
    public class Human : Animal
    {
        public UsAddress HumanAddress { get; set; }
    }
    
    public class Horse : Animal
    {
        public UsAddress HorseAddress { get; set; }
        public IList<UsAddress> HorseAddresses { get; set; }
    }
    public class Address
    {
      public City City { get; set; }
    }
    
    public class UsAddress : Address
    {
      public City SubCity { get; set; }
    }

    Add navigation property binding:

    customers.HasManyPath((VipCustomer v) => v.VipLocations).HasRequiredBinding(a => a.City, "A");
    customers.HasManyPath((VipCustomer v) => v.VipLocations).HasRequiredBinding((UsAddress a) => a.SubCity, "B");
    
    var pet = customers.HasRequiredPath(c => c.Pet);
    
    pet.HasRequiredPath((Human p) => p.HumanAddress)
        .HasRequiredBinding(c => c.SubCity, "HumanCities");
    
    pet.HasRequiredPath((Horse h) => h.HorseAddress).HasRequiredBinding(c => c.SubCity, "HorseCities");
    
    pet.HasManyPath((Horse h) => h.HorseAddresses).HasRequiredBinding(c => c.SubCity, "HorseCities");
      

    So, we can get the following target binding:

    <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
      <EntityContainer Name="Container">
        <EntitySet Name="Customers" EntityType="System.Web.OData.Builder.Cusomter">
          <NavigationPropertyBinding Path="Pet/System.Web.OData.Builder.Human/HumanAddress/SubCity" Target="HumanCities" />
          <NavigationPropertyBinding Path="Pet/System.Web.OData.Builder.Horse/HorseAddress/SubCity" Target="HorseCities" />
          <NavigationPropertyBinding Path="Pet/System.Web.OData.Builder.Horse/HorseAddresses/SubCity" Target="HorseCities" />
          <NavigationPropertyBinding Path="System.Web.OData.Builder.VipCustomer/VipLocations/System.Web.OData.Builder.UsAddress/SubCity" Target="B" />
          <NavigationPropertyBinding Path="System.Web.OData.Builder.VipCustomer/VipLocations/City" Target="A" />
        </EntitySet>
        <EntitySet Name="A" EntityType="System.Web.OData.Builder.City" />
        <EntitySet Name="B" EntityType="System.Web.OData.Builder.City" />
        <EntitySet Name="HumanCities" EntityType="System.Web.OData.Builder.City" />
        <EntitySet Name="HorseCities" EntityType="System.Web.OData.Builder.City" />
      </EntityContainer>
    </Schema>
    

    Where: As example shown:

    1. It supports single property with multiple binding path.
    2. It supports collection property with mutliple binding path.
    3. It also supports mulitple binding path with inheritance type.

    Besides, the HasManyPath()/HasSinglePath() has onverload methods to configure the containment binding path.

    Multiple navigation property binding path in convention model builder

    In convention model builder, it will automatically traverl all property binding paths to add a binding for the navigation proeprty.

    There’s an unit test that you can refer to.

  • 13.4 Dependency Injection Support

    Since Web API OData V6.0.0 beta, we have integrated with the popular dependency injection (DI) framework Microsoft.Extensions.DependencyInjection. By means of DI, we can significantly improve the extensibility of Web API OData as well as simplify the APIs exposed to the developers. Meanwhile, we have incorporated DI support throughout the whole OData stack (including ODataLib, Web API OData and RESTier) thus the three layers can consistently share services and custom implementations via the unified DI container in an OData service. For example, if you register an ODataPayloadValueConverter in a RESTier API class, the low-level ODataLib will be aware of that and use it automatically because they share the same DI container.

    For the fundamentals of DI support in OData stacks, please refer to this docs from ODataLib. After understanding that, we can now take a look at how Web API OData implements the container, takes use of it and injects it into ODataLib.

    Implement the Container Builder

    By default, if you don’t provide a custom container builder, Web API OData will use the DefaultContainerBuilder which implements IContainerBuilder from ODataLib. The default implementation is based on the Microsoft DI framework introduced above and what it does is just delegating the builder operations to the underlying ServiceCollection.

    But if you want to use a different DI framework (e.g., Autofac) or make some customizations to the default behavior, you will need to either implement your own container builder from IContainerBuilder or inherit from the DefaultContainerBuilder. For the former one, please refer to the docs from ODataLib. For the latter one, here is a simple example to illustrate how to customize the default container builder.

    public class MyContainerBuilder : DefaultContainerBuilder
    {
        public override IContainerBuilder AddService(ServiceLifetime lifetime, Type serviceType, Type implementationType)
        {
            if (serviceType == typeof(ITestService))
            {
                // Force the implementation type of ITestService to be TestServiceImpl.
                base.AddService(lifetime, serviceType, typeof(TestServiceImpl));
            }
    
            return base.AddService(lifetime, serviceType, implementationType);
        }
    
        public override IServiceProvider BuildContainer()
        {
            return new MyContainer(base.BuildContainer());
        }
    }
    
    public class MyContainer : IServiceProvider
    {
        private readonly IServiceProvider inner;
    
        public MyContainer(IServiceProvider inner)
        {
            this.inner = inner;
        }
    
        public object GetService(Type serviceType)
        {
            if (serviceType == typeof(ITestService))
            {
                // Force to create a TestServiceImpl2 instance for ITestService.
                return new TestServiceImpl2();
            }
    
            return base.GetService(serviceType);
        }
    }

    After implementing the container builder, you need to register that container builder in HttpConfiguration to tell Web API OData that you want to use your custom one. Please note that you MUST call UseCustomContainerBuilder BEFORE MapODataServiceRoute and EnableDependencyInjection because the root container will be actually created in these two methods. Setting the container builder factory after its creation is meaningless. Of course, if you wish to keep the default container builder implementation, UseCustomContainerBuilder doesn’t need to be called at all.

    configuration.UseCustomContainerBuilder(() => new MyContainerBuilder());
    configuration.MapODataServiceRoute(...);

    Register the Required Services

    Basic APIs to register the services have already been documented here. Here we mainly focus on the APIs from Web API OData that help to register the services into the container builder. The key API to register the required services for an OData service is an overload of MapODataServiceRoute which takes a configureAction to configure the container builder (i.e., register the services).

    public static class HttpConfigurationExtensions
    {
        public static ODataRoute MapODataServiceRoute(this HttpConfiguration configuration, string routeName,
                string routePrefix, Action<IContainerBuilder> configureAction);
    }

    Theoretically you can register any service within the configureAction but there are two mandatory services that you are required to register: the IEdmModel and a collection of IRoutingConvention. Without them, the OData service you build will NOT work correctly. Here is an example of calling the API where a custom batch handler MyBatchHandler is registered. You are free to register any other service you like to the builder.

    configuration.MapODataServiceRoute(routeName: "odata", routePrefix: "odata", builder =>
        builder.AddService<IEdmModel>(ServiceLifetime.Singleton, sp => model)
               .AddService<ODataBatchHandler, MyBatchHandler>(ServiceLifetime.Singleton)
               .AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp =>
                   ODataRoutingConventions.CreateDefaultWithAttributeRouting(routeName, configuration)));

    You might also find that we still preserve the previous overloads of MapODataServiceRoute which take batch handlers, path handlers, HTTP message handlers, etc. They are basically wrapping the first overload that takes a configureAction. The reason why we keep them is that we want to give the users convenience to create OData services and bearings to the APIs they are familiar with.

    Once you have called any of the MapODataServiceRoute overloads, the dependency injection for that OData route is enabled and an associated root container is created. As we internally maintain a dictionary to map the route name to its corresponding root container (1-1 mapping), multiple OData routes (i.e., calling MapODataServiceRoute multiple times) are still working great and the services registered in different containers (or routes) will not impact each other. That said, if you want a custom batch handler to work in the two OData routes, register them twice.

    Enable Dependency Injection for HTTP Routes

    It’s also possible that you don’t want to create OData routes but just HTTP routes. The dependency injection support will NOT be enabled right after you call MapHttpRoute. In this case, you have to call EnableDependencyInjection to enable the dependency injection support for ALL HTTP routes. Please note that all the HTTP routes share the SAME root container which is of course different from the one of any OData route. That said calling EnableDependencyInjection has nothing to do with MapODataServiceRoute.

    configuration.MapHttpRoute(...);
    configuration.EnableDependencyInjection();

    Please also note that the order of MapHttpRoute and EnableDependencyInjection doesn’t matter because they have no dependency on each other.

    Manage and Access the Request Container

    Given a root container, we can create scoped containers from it, which is also known as request containers. Mostly you don’t need to manage the creation and destruction of request containers yourself but there are some rare cases you have to touch them. Say you want to implement your custom batch handler, you have the full control of the multi-part batch request. You parse and split it into several batch parts (or sub requests) then you will be responsible for creating and destroying the request containers for the parts. They are implemented as extension methods to HttpRequestMessage in HttpRequestMessageExtensions.

    To create the request container, you need to call the following extension method on a request. If you are creating the request container for a request that comes from an HTTP route, just pass null for the routeName.

    public static class HttpRequestMessageExtensions
    {
        // Example:
        //   IServiceProvider requestContainer = request.CreateRequestContainer("odata");
        //   IServiceProvider requestContainer = request.CreateRequestContainer(null);
        public static IServiceProvider CreateRequestContainer(this HttpRequestMessage request, string routeName);
    }

    To delete the request container from a request, you need to call the following extension method on a request. The parameter dispose indicates whether to dispose that request container after deleting it from the request. Disposing a request container means that all the scoped and transient services within that container will also be disposed if they implement IDisposable.

    public static class HttpRequestMessageExtensions
    {
        // Example:
        //   request.DeleteRequestContainer(true);
        //   request.DeleteRequestContainer(false);
        public static void DeleteRequestContainer(this HttpRequestMessage request, bool dispose)
    }

    To get the request container associated with that request, simply call the following extension method on a request. Note that you don’t need to provide the route name to get the request container because the container itself has already been stored in the request properties during CreateRequestContainer. There is also a little trick in GetRequestContainer that if you have never called CreateRequestContainer on the request but directly call GetRequestContainer, it will try to create the request container for all the HTTP routes and return that container. Thus the return value of GetRequestContainer should never be null.

    public static class HttpRequestMessageExtensions
    {
        // Example:
        //   IServiceProvider requestContainer = request.GetRequestContainer();
        public static IServiceProvider GetRequestContainer(this HttpRequestMessage request)
    }

    Please DO pay attention to the lifetime of the services. DON’T forget to delete and dispose the request container if you create it yourself. And scoped services will be disposed after the request completes.

    Services Available in Web API OData

    Currently services Available in Web API OData include:

    • IODataPathHandler whose default implementation is DefaultODataPathHandler and lifetime is Singleton.
    • XXXQueryValidator whose lifetime are all Singleton.
    • ODataXXXSerializer and ODataXXXDeserializer whose lifetime are all Singleton. But please note that they are ONLY effective when DefaultODataSerializerProvider and DefaultODataDeserializerProvider are present. Custom serializer and deserializer providers are NOT guaranteed to call those serializers and deserializers from the DI container.
    • ODataSerializerProvider and ODataDeserializerProvider whose implementation types are DefaultODataSerializerProvider and DefaultODataDeserializerProvider respectively and lifetime are all Singleton. Please note that you might lose all the default serializers and deserializers registered in the DI container if you don’t call into the default providers in your own providers.
    • IAssembliesResolver whose implementation type is the default one from ASP.NET Web API.
    • FilterBinder whose implementation type is Transient because each EnableQueryAttribute instance will create its own FilterBinder. Override it if you want to customize the process of binding a $filter syntax tree.

    Services Avaliable in OData Lib

    Services in OData Lib also can be injected through Web API OData.

  • 13.5 Use ODL path classes

    Since Web API OData V6.0.0 beta, Web API OData uses the ODL path classes directly.

    In Web API OData V5.x, there are a lot of path segment classes defined to wrapper the corresponding path segment classes defined in ODL. for example:

    * BatchPathSegment
    * BoundActionPathSegment
    * BoundFunctionPathSegment
    * KeyValuePathSegment
    * ...

    Where,

    1. Some segments are just used to wrapper the corresponding segments defined in ODL, such as BatchPathSegment.
    2. Some segments are used to do some conversions, such as BoundFunctionPathSegment, KeyValuePathSegment.

    However, ODL defines the same segments and Web API OData needn’t to do such conversion. So, all segment classes defined in Web API OData are removed in v6.x.

    Web API OData will directly use the path segment classes defined in ODL as follows:

    * BatchReferenceSegment
    * BatchSegment
    * CountSegment
    * DynamicPathSegment
    * EntitySetSegment
    * KeySegment
    * MetadataSegment
    * NavigationPropertyLinkSegment
    * NavigationPropertySegment
    * ODataPathSegment
    * OperationImportSegment
    * OperationSegment
    * PathTemplateSegment
    * PropertySegment
    * SingletonSegment
    * TypeSegment
    * ValueSegment
  • 13.6 Key value binding

    Since Web API OData V6.0.0 beta, Web API OData supports the composite key convention binding.

    Let’s have an example:

    public class Customer
    {
        public string StringProp { get; set; }
    	
        public Date DateProp { get; set; }
    
        public Guid GuidProp { get; set; }
    }

    Where, Customer is an entity type with three properties. We will make all these trhee properties as the composite keys for Customer entity type.

    So, we can do:

    private static IEdmModel GetEdmModel()
    {
        ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntityType<Customer>().HasKey(c =>new { c.StringProp, c.DateProp, c.GuidProp});
    	builder.EntitySet<Customer>("Customers");
    	return builder.GetEdmModel();
    }

    Before Web API OData V6.x, key segment convention routing only supports the single key convention binding, just use the key as the parameter name.

    In Web API OData V6.x, we use the following convention for the composite key parameter name, but leave the key for single key parameter.

    "key" + {CompositeKeyPropertyName}

    Therefore, for StringProp key property, the action parameter name should be keyStringProp.

    Let’s see how the contoller action looks like:

    public class CustomersController : ODataController
    {
        public IHttpActionResult Get([FromODataUri]string keyStringProp, [FromODataUri]Date keyDateProp, [FromODataUri]Guid keyGuidProp)
        {
        }
    }

    Now, you can issue a request:

    GET http://~/odata/Customers(StringKey='my',DateKey=2016-05-11,GuidKey=46538EC2-E497-4DFE-A039-1C22F0999D6C)

    The result is:

    1. keyStringProp == "my";
    2. keyDataProp == new Date(2016, 5, 11);
    3. keyGuidProp == new Guid("46538EC2-E497-4DFE-A039-1C22F0999D6C")

    Be noted, this rule also applys to the navigation property key convention binding.

  • 13.7 Merge complex & entity value serialize/deserialize

    You can refer the detail in: complex type and entity type in ODL 7.0 doc.

    In Web API OData 6.x, we do the following to support the merge of complex type and entity type:

    • Rename ODataFeed to ODataResourceSet
    • Rename ODataEntry to ODataResource
    • Rename ODataNavigationLink to ODataNestedResourceInfo
    • Rename ODataPayloadKind.Entry to ODataPayloadKind.Resource
    • Rename ODataPayloadKind.Feed to ODataPayloadKind.ResourceSet
    • Rename ODataEntityTypeSerializer to ODataResourceSerializer
    • Rename ODataFeedSerializer to ODataResourceSetSerizlier
    • Rename ODataEntityDeserializer to ODataResourceDeserializer
    • Rename ODataFeedDeserializer to ODataResourceSetDeserializer
    • Remove ODataComplexValue
    • Remove ODataComplexSerializer/ODataComplexTypeDeserializer

    So, for any complex value, it will use ODataResourceSerializer and ODataResourceDeserializer to read and write. for any complex collection value, it will use ODataResourceSetSerializer and ODataResourceSetDeserializer to read and write.

14. 7.X FEATURES

  • 14.1 7.0 Beta1 & Beta2 & Beta4

    Web API OData Beta1, Beta2 & Beta4 includes a new package for WebApi OData V7.0.0 for ASP.NET Core 2.x. The nightly version of this package is available from this nightly url.

    There is also a new WebApi OData V7.0.0 for ASP.NET Framework. The nightly version of this package is available from this nightly url. The APIs are the same as those in WebApi OData V6.x, except for the new namespace Microsoft.AspNet.OData for V7, which is changed from System.Web.OData.

    Both packages depends on OData Lib 7.0.0.

    The code for the packages can be found here

    Known Issues

    Web API OData for ASP.NET Core Beta1, has following limitations which are known issues:

    • Batching is not fully supported
    • Using EnableQuery in an HTTP route, i.e. non-OData route, is not fully functional
    • #1175 - When you first start your service under a debugger, the project app URL will likely make a request on a non-OData route. This will fail with an exception Value cannot be null. Parameter name: routeName. You can work around this issue by adding routes.EnableDependencyInjection(); in UseMvc() lambda in Configure. You can configure the default startup request in Project properties, Debug, App URL.

    Web API OData for ASP.NET, there are no known issues.

    OData V7.0.0 for ASP.NET Core 2.x

    The new OData V7.0.0 for ASP.NET Core 2.x package supports the same features set as Web API OData V6.0.0 but works with ASP.NET Core. You can learn more about ASP.NET Core from the documentation.

    To get started with OData V7.0.0 for ASP.NET Core 2.x, you can use code that is very similar to Web API OData V6.0.0. All of the documentation in Writing a simple OData V4 service is correct except for configuring the OData endpoint. Instead of using the Register() method, you’ll follow the new service + route configuration model used in ASP.NET Core.

    The namespace for both Web API OData packages is Microsoft.AspNet.OData.

    a. Create the Visual Studio project

    In Visual Studio 2017, create a new C# project from the ASP.NET Core Web Application template. Name the project “ODataService”.

    In the New Project dialog, select ASP.NET Core 2.0 and select the WebApi template. Click OK.

    b. Install the OData packages

    In the Nuget Package Manager, install Microsoft.AspNetCore.OData and all it’s dependencies.

    c. Add a model class

    Add a C# class to the Models folder:

    namespace ODataService.Models
    {
        public class Product
        {
            public int ID { get; set; }
            public string Name { get; set; }
        }
    }

    d. Add a controller class

    Add a C# class to the Controllers folder:

    namespace ODataService.Controllers
    {
        public class ProductsController : ODataController
        {
            private List<Product> products = new List<Product>()
            {
                new Product()
                {
                    ID = 1,
                    Name = "Bread",
                }
            };
    
            [EnableQuery]
            public List<Product> Get()
            {
                return products;
            }
        }
    }

    In the controller, we defined a List<Product> object which has one product element. It’s considered as an in-memory storage of the data of the OData service.

    We also defined a Get method that returns the list of products. The method refers to the handling of HTTP GET requests. We’ll cover that in the sections about routing.

    This Get method is decorated with EnableQueryAttribute, which in turns supports OData query options, for example $expand, $filter etc.

    e. Configure the OData Endpoint

    Open the file Startup.cs. Replace the existing ConfigureServices and Configure methods with the following code:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
        services.AddOData();
    }
    
    public void Configure(IApplicationBuilder app)
    {
        var builder = new ODataConventionModelBuilder(app.ApplicationServices);
    
        builder.EntitySet<Product>("Products");
    
        app.UseMvc(routeBuilder =>
        {
            // and this line to enable OData query option, for example $filter
            routeBuilder.Select().Expand().Filter().OrderBy().MaxTop(100).Count();
    
            routeBuilder.MapODataServiceRoute("ODataRoute", "odata", builder.GetEdmModel());
    
            // uncomment the following line to Work-around for #1175 in beta1
            // routeBuilder.EnableDependencyInjection();
        });
    }

    f. Start the OData service

    Start the OData service by running the project and open a browser to consume it. You should be able to get access to the service document at http://host/odata/ in which http://host/odata/ is the root path of your service. The metadata document can be accessed at GET http://host:port/odata/$metadata and the products at GET http://host:port/odata/Products where host:port is the host and port of your service, usually something like localhost:1234.

    g. Explore

    As mentioned earlier, most of the samples for Web API OData V6.0.0 apply to Web API OData V7.0.0. One of the design goals was to keep the API between the two as similar as possible. While the APIs are similar, they are not identical due to differences between ASP.NET and ASP.NET Core, such as HttpRequestMessage is now HttpRequest.

    ODataRoutingConvention

    Both Microsoft.AspNet.OData and Microsoft.AspNetCore.OData packages support same set of OData routing conventions, including default built-in routing conventions and attribute rounting convention, so that each request can be routed to matching controller for processing. All routing conventions implement the interface IODataRoutingConvention, however, with different definitions, as highlighted below, for the two packages due to different route matching implementations based on ASP.NET Framework and ASP.NET Core:

    • For ASP.NET Framework:
    namespace Microsoft.AspNet.OData.Routing.Conventions
    {
        /// <summary>
        /// Provides an abstraction for selecting a controller and an action for OData requests.
        /// </summary>
        public interface IODataRoutingConvention
        {
            /// <summary>
            /// Selects the controller for OData requests.
            /// </summary>
            string SelectController(ODataPath odataPath, HttpRequestMessage request);
    
            /// <summary>
            /// Selects the action for OData requests.
            /// </summary>
            string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, ILookup<string, HttpActionDescriptor> actionMap);
        }
    }
    • For ASP.NET Core 2.x:
    namespace Microsoft.AspNet.OData.Routing.Conventions
    {
        /// <summary>
        /// Provides an abstraction for selecting a controller and an action for OData requests.
        /// </summary>
        public interface IODataRoutingConvention
        {
            /// <summary>
            /// Selects the controller and action for OData requests.
            /// </summary>        
            IEnumerable<ControllerActionDescriptor> SelectAction(RouteContext routeContext);
        }
    }

    Specific routing convention, e.g. MetadataRoutingConvention, typically implements the package-specific interface, provides package-specific implementation, and shares common logic for both platform in the Microsoft.AspNet.OData.Shared shared project.

  • 14.2 Simplified optional-$-prefix for OData query option for WebAPI query parsing

    Since ODL-6.x, OData Core Library supports query option with optional-$-prefix as described in this docs.

    Corresponding support on WebAPI layer is available starting WebAPI-7.4.

    As result, WebAPI is able to process OData system query with optional $-prefix, as in “GET ~/?filter=id eq 33” with injected dependency setting:

        ODataUriResolver.EnableNoDollarQueryOptions = true.
    

    ODL Enhancement

    A public boolean attribute EnableNoDollarQueryOptions is added to ODataUriResolver. Public attribute is needed for dependency injection on the WebAPI layer.

        public class ODataUriResolver
        {
            ...
            public virtual bool EnableNoDollarQueryOptions { get; set; }
            ...
        }
    

    WebAPI optional-$-prefix Setting using Dependency Injection

    WebAPI service injects the setting using the ODataUriResolver during service initialization: Builder of service provider container sets the instantiated ODataUriResover config using dependency injection.

                ODataUriResolver resolver = new ODataUriResolver
                {
                    EnableNoDollarQueryOptions = true,
                    EnableCaseInsensitive = enableCaseInsensitive
    
                };
                
                spContainerBuilder.AddService(ServiceLifetime.Singleton, sp => resolver));
    

    Note that UriResolver is typically a singleton for the service instance, since each instance should follow the same Uri convention. In case of other injected dependencies that are configurable per request, scoped dependency should be used.

    WebAPI Internal Processing of optional-$-prefix Setting

    1. WebAPI EnableQuery attribute processing instantiates WebAPI’s ODataQueryOptions object for incoming request.
    2. The ODataQueryOptions constructor pins down the optional-$-prefix setting (see _enableNoDollarSignQueryOptions) from the injected ODataUriResolver.
    3. Based on the optional-$-prefix setting, ODataQueryOptions parses the request Uri in WebAPI layer accordingly.
  • 14.3 WebApi URI parser default setting updates: case-insensitive names and unqualified functions & actions

    OData Core Library v7.x has introduced the following two usability improvement:

    • Uri parsing with case-insensitive name, and

    • Unqualified functions & actions, which are not required to have namespace prefix.

    Starting with WebAPI OData v7.0, these two behaviors are supported by default.

    Examples

    Prior to WebApi v7.0, for example, the following Uris are supported:

    • GET /service/Customers?$filter=Id eq 5

    • POST /service/Customers(5)/Default.UpdateAddress()

    With WebApi v7.0 by default, in addition to above, the following variances are also supported:

    • GET /service/Customers?$filter=id eq 5

    • GET /service/CUSTOMERS?$filter=Id eq 5

    • POST /service/Customers(5)/UpdateAddress()

    and the combination of both case-insensitive and unqualified functions & actions, such as:

    • POST /service/CUSTOMERS(5)/UpdateAddress()

    Backward Compatibility

    Case-insensitive semantics is supported for type name, property name and function/action name. WebAPI OData will first try to resolve the name with case-sensitive semantics and return the best match if found; otherwise case-insensitive semantics are applied, returning the unique match or throwing an exception if multiple case-insensitive matches exist.

    With support for unqualified function & action, the URI parser will do namespace-qualified function & action resolution when the operation name is namespace-qualified; otherwise all namespaces in the customer’s model are treated as default namespaces, returning the unique match or throwing an exception if multiple unqualified matches exist.

    Because of the precedence rules applied, scenarios supported in previous versions of WebAPI continue to be supported with the same semantics, while new scenarios that previously returned errors are also are now supported.

    Please note that, even though case-insensitive and unqualified function & action support is added as a usability improvement, services are strongly encouraged to use names that are unique regardless of case, and to avoid naming bound functions, actions, or derived types with the same name as a property of the bound type. For example, a property and unqualified function with same name would resolve to a property name when the unqualified function may have been expected.

    Restoring the original behavior

    Even though the new behavior is backward compatible for most scenarios, customers can configure WebAPI to enforce case sensitivity and namespace qualification, as in 6.x, using dependency injection:

        // HttpConfiguration configuration
        IServiceProvider rootContainer = configuration.CreateODataRootContainer(routeName, 
            builder => builder.AddService<ODataUriResolver>(ServiceLifetime.Singleton, sp => new ODataUriResolver());
    

    The above code replaces the ODataUriResolver service that supports case-insensitivity and unqualified names with a default instance of ODataUriResolver that does not.

  • 14.4 OData Web API 7.0 (.NET Core and .NET Classic)

    We’re happy to announce the release of ASP.NET Web API OData 7.0 on the NuGet gallery!

    ASP.NET Web API OData 7.0 is available in two packages:

    1. Microsoft.AspNetCore.OData is based on ASP.NET Core 2.0.
    2. Microsoft.AspNet.OData is based on ASP.NET Web API.

    Detail information on Web API OData 7.0 is available here

    Get started with ASP.NET Web API OData 7.0 today!

    Download this release

    You can install or update the NuGet packages for OData Web API v7.0.0 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNetCore.OData
    

    or

    PM> Install-Package Microsoft.AspNet.OData
    

    What’s in this release?

    ASP.NET Core 2.0 support

    See main issues from:

    Getting started ASP.NET Core OData from here, get samples from here.

    New features

    • [ PR #1497 ] Support In operator.

    • [ PR #1409 ] Set default to Unqualified-function/acition call and case insensitive.

    • [ PR #1393 ] Set default to enable KeyAsSegment.

    • [ Issue #1503 ] Enable support for Json Batch.

    • [ Issue #1386 ] Use TemplateMatcher for ODataBatchPathMapping.

    • [ Issue #1248 ] Allow recursive complex types in OData model builder.

    • [ Issue #1225 ] Support optional dollar sign.

    • [ Issue #893 ] Support aggregation of Entity Set.

    Breaking changes

    Improvements & Fixes:

    • [ Issue #1510 ] move $top & $skip execute as late as possible.

    • [ Issue #1489 ] Return Task<> from method of controller doesn’t pick up odata output formatter (Core).

    • [ Issue #1407 ] Fix the BaseAddressFactory in ODataMediaTypeFormatter .

    • [ Issue #1471 ] Issue in non-odata routing with DataContact & DataMember.

    • [ Issue #1434 ] Add OData-Version into the No-Content response header.

    • [ Issue #1398 ] Expose underlying semantic OData path.

    • [ Issue #1388 ] Make Match as virtual in ODataPathRouteConstraint.

    • [ Issue #1387 ] Move Instance to select all.

    • [ Issue #1313 ] Batch requests are incorrectly routed when ASP.NET Core OData web application has any BasePath.

    • [ Issue #1263 ] Patch nested strucutural resources.

    • [ Issue #1247 ] Fix Spatial post/update problem.

    • [ Issue #1113 ] ODataResourceDeserializer no longer supports null ComplexType values.

    • [ Issue #822 ] Fix for memory leak in EdmLibHelpers.


    Questions and feedback

    You and your team are warmly welcomed to try out this new version if you are interested in the new features and fixes above. You are also welcomed to contribute your code to OData Web API repository. For any feature request, issue or idea please feel free to reach out to us at GitHub Issues.

  • 14.5 OData Web API 7.0.1 (.NET Core and .NET Classic)

    The NuGet packages for ASP.NET Web API OData v7.0.1 are available on the NuGet gallery.

    You can install or update the NuGet packages for OData Web API v7.0.1 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNetCore.OData -Version 7.0.1 
    

    or

    PM> Install-Package Microsoft.AspNet.OData -Version 7.0.1
    

    What’s in this release?

    New Features:

    Improvements and fixes:


    Questions and feedback

    You and your team are warmly welcomed to try out this new version if you are interested in the new features and fixes above. You are also welcomed to contribute your code to OData Web API repository. For any feature request, issue or idea please feel free to reach out to us at GitHub Issues.

  • 14.6 OData Web API 7.1.0 (.NET Core and .NET Classic)

    The NuGet packages for ASP.NET Web API OData v7.1.0 are available on the NuGet gallery.

    You can install or update the NuGet packages for OData Web API v7.1.0 using the Package Manager Console:

    PM> Install-Package Microsoft.AspNetCore.OData -Version 7.1.0 
    

    or

    PM> Install-Package Microsoft.AspNet.OData -Version 7.1.0
    

    Important Notes:

    issue #1591 fixes an issue where types created by the ODataModelBuilder did not respect the namespace of the ModelBuilder and instead used the namespace of the CLR type. With PR #1592, OData WebAPI 7.1.0 now correctly uses the namespace on the ModelBuilder, if it has been explicitly set. In order to retain the old behavior of using the namespace of the CLR type, do not set the namespace on the ModelBuilder, or set the namespace on the ModelBuilder to null or to the desired namespace of the CLR type.

    What’s in this release?

    New Features:

    • [ #1631 ] Don’t require keys for singleton instances

    • [ #1628 ] Allow adding new members to a collection through a POST request

    • [ #1591 ] Support namespaces in OData ModelBuilder

    • [ #1656 ] Allowing the definition of partner relationships

    Improvements and fixes:

    • [ #1543 ] Json batch response body if 404

    • [ #1555 ] aspnetcore ETagMessageHandler throws then ClrType property name and EdmModel property name differs

    • [ #1559 ] Don’t assume port 80 for nextLink when request was HTTPS

    • [ #1579 ] Star select ( $select= * ) not returning dynamic properties

    • [ #1588 ] Null check on ODataQueryParameterBindingAttribute for Net Core

    • [ #736 ] EDMX returned from $metadata endpoint has incorrect “Unicode” attributes

    • [ #850 ] ODataConventionModelBuilder takes into account [EnumMember] on enum members, but ODataPrimitiveSerializer (?) does not, causing mismatch in the payload.

    • [ #1612 ] Add the iconUrl to the nuget spec

    • [ #1615 ] fix compile error in sample project

    • [ #1421 ] Improve error message when structured type is received and properties are in incorrect case

    • [ #1658 ] Fix the security vulnerabilities in project dependencies

    • [ #1637 ] Change Request: Remove null check on ‘ODataFeature().TotalCount’ property

    • [ #1673 ] Respect preference header for paging

    • [ #1600 ] ActionDescriptorExtensions is not thread safe

    • [ #1617 ] Take and Top query use wrong Provider.CreateQuery Method

    • [ #1659 ] Odata V4 group by with Top and skip not working

    • [ #1679 ] Fix typo routePrerix


    Questions and feedback

    You and your team are warmly welcomed to try out this new version if you are interested in the new features and fixes above. You are also welcomed to contribute your code to OData Web API repository. For any feature request, issue or idea please feel free to reach out to us at GitHub Issues.