1. GETTING STARTED

  • 1.1 Introduction

    OData stands for the Open Data Protocol. It was initiated by Microsoft and is now an ISO and OASIS standard. OData enables the creation and consumption of RESTful APIs, which allow resources, defined in a data model and identified by using URLs, to be published and edited by Web clients using simple HTTP requests.

    RESTier is a RESTful API development framework for building standardized, OData V4 based RESTful services on .NET platform. It can be seen as a middle-ware on top of Web API OData. RESTier provides facilities to bootstrap an OData service like what WCF Data Services (which is sunset) does, beside this, it supports to add business logic in several simple steps, has flexibility and easy customization like what Web API OData do. It also supports to add additional publishers to support other protocols and additional providers to support other data sources.

    For more information about OData, please refer to the following resources:

    OData.org

    OASIS Open Data Protocol (OData) Technical Committee

    For more information about OData .Net Library, refer to OData .Net Library document.

    For more information about Web API OData Library, refer to Web API OData Library document.

  • 1.2 Bootstrap an OData service [>=0.4.0]

    After RESTier 0.4.0, creating an OData service has never been easier! This subsection shows how to create an OData V4 endpoint using RESTier in a few minutes. AdventureWorksLT will be used as the sample database and Entity Framework as the data proxy.

    Create a project and a web app

    1.Open Visual Studio 2015 or Visual Studio 2013. If you use Visual Studio 2013, the screens will be slightly different from the screenshots, but the procedures are essentially the same.

    2.From the File menu, click New > Project.

    3.In the New Project dialog box, click C# > Web > ASP.NET Web Application.

    4.Clear the Add Application Insights to Project check box.

    5.Name the application HelloWorld.

    6.Click OK.

    7.In the New ASP.NET Project dialog box, select the Empty template.

    8.Select the Web API check box.

    9.Clear the Host in the cloud check box.

    Install the RESTier packages

    1.In the Solution Explorer window, right click the project HelloWorld and select Manage NuGet Packages….

    2.In the NuGet Package Manager window, select the Include prerelease checkbox.

    3.Type Restier in the Search Box beside and press Enter.

    4.Select Microsoft.Restier and click the Install button.

    5.In the Preview dialog box, click the OK button.

    6.In the License Acceptance dialog box, click the I Accept button.

    Generate the model classes

    1.Download AdventureWorksLT2012_Data.mdf and import it into the (localdb)\MSSQLLocalDB database.

    2.In the Solution Explorer window, right click the Models folder under the project HelloWorld and select Add > New Item.

    3.In the Add New Item - HelloWorld dialog box, click C# > Data > ADO.NET Entity Data Model.

    4.Name the model AdventureWorksLT.

    5.Click the Add button.

    6.In the Entity Data Model Wizard window, select the item Code First from database.

    7.Click the Next button.

    8.Click the New Connection button.

    9.In the Connection Properties dialog box, type (localdb)\MSSQLLocalDB for Server name.

    10.Select AdventureWorksLT2012 for database name.

    11.After returning to the Entity Data Model Wizard window, click the Next button.

    12.Select the Tables check box and click the Finish button.

    Configure the OData Endpoint

    In the Solution Explorer window, click HelloWorld > App_Start > WebApiConfig.cs. Replace the WebApiConfig class the following code.

    using System.Web.Http;
    using HelloWorld.Models;
    using Microsoft.Restier.Providers.EntityFramework;
    using Microsoft.Restier.Publishers.OData;
    using Microsoft.Restier.Publishers.OData.Batch;
    using Microsoft.Restier.Publishers.OData.Routing;
    
    namespace HelloWorld
    {
        public static class WebApiConfig
        {
            public async static void Register(HttpConfiguration config)
            {
                await config.MapRestierRoute<EntityFrameworkApi<AdventureWorksLT>>(
                    "AdventureWorksLT",
                    "api/AdventureWorksLT",
                    new RestierBatchHandler(GlobalConfiguration.DefaultServer));
            }
        }
    }

    Note : DbApi was renamed to EntityFrameworkApi from version 0.5.

    After these steps, you will have finished bootstrapping an OData service endpoint. You can then Run the project and an OData service is started. Then you can start by accessing the URL http://localhost:<ISS Express port>/api/AdventureWorksLT to view all available entity sets, and try with other basic OData CRUD operations. For instance, you may try querying any of the entity sets using the $select, $filter, $orderby, $top, $skip or $apply query string parameters.

2. FEATURES

  • 2.1 Security (>=0.4.0)

    Authentication (>=0.4.0)

    REStier is transparent to security now, any security configurations / methodology working for Web APi will work for RESTier.

    RESTier Role Based Security [0.4.0-beta only]

    Currently the quality of the RESTier role-based security module is NOT guaranteed and this module will NOT be shipped with regular RESTier releases and will not be part of first GA release. Please use it at your own risk.

    For example code, please refer to NorthwindApi.

  • 2.2 Entity Set Filters [>=0.4.0]

    Entity set filter convention helps plug in a piece of filtering logic for entity set. It is done via adding an OnFilter[entity set name](IQueryable<T> entityset) method to the Api class.

    1. The filter method name must be OnFilter[entity set name], ending with the target entity set name.
    2. It must be a **protected** method on the `Api` class.
    3. It should accept an IQueryable<T> parameter and return an IQueryable<T> result where T is the entity type. 
    

    Supposed that ~/AdventureWorksLT/Products can get all the Product entities, the below OnFilterProducts method will filter some Product entities by checking the ProductID.

    using Microsoft.Restier.Core;
    using Microsoft.Restier.Provider.EntityFramework;
    using System.Data.Entity;
    using System.Linq;
    using System.Threading.Tasks;
    
    namespace AdventureWorksLTSample.Models
    {
        public class AdventureWorksApi : EntityFrameworkApi<AdventureWorksContext>
        {
            protected IQueryable<Product> OnFilterProducts(IQueryable<Product> entitySet)
            {
                return entitySet.Where(s => s.ProductID % 3 == 0).AsQueryable();
            }
        }
    }

    Then replace the EntityFrameworkApi<AdventureWorksLT> in WebApiConfig with AdventureWorksApi, Now some testings will show that:

    1. ~/AdventureWorksLT/Products will only get the Product entities whose ProductID is  3,6,9,12,15,... 
    2. ~/AdventureWorksLT/Products([product id]) will only be able to get a Product entity whose ProductID mod 3 results a zero. 
    
  • 2.3 Submit Logic [>=0.4.0]

    Submit logic convention allows users to authorize a submit operation or plug in user logic (such as logging) before and after a submit operation. Usually a submit operation can be inserting an entity, deleting an entity, updating an entity or executing an OData action.

    For version 0.5.0 or newer, customize submit logic with single class for all entity set is supported, refer to section 2.9 for more detail.

    Authorization

    Users can control if one of the four submit operations is allowed on some entity set or action by putting some protected methods into the Api class. The method signatures must exactly match the following examples. The method name must conform to Can<Insert|Update|Delete|Execute><EntitySetName|ActionName>.

    namespace Microsoft.OData.Service.Sample.Trippin.Api
    {
        public class TrippinApi : EntityFrameworkApi<TrippinModel>
        {
            ...
            // Can delete an entity from the entity set Trips?
            protected bool CanDeleteTrips()
            {
                return false;
            }
            
            // Can execute an action named ResetDataSource?
            protected bool CanExecuteResetDataSource()
            {
                return false;
            }
        }
    }

    Plug in user logic

    Users can plug in user logic before and after executing one of the four submit operations by putting similar protected methods into the Api class. The method signatures must also exactly match the following examples. The method name must conform to On<Insert|Updat|Delet|Execut><ed|ing><EntitySetName|ActionName> where ing for before submit and ed for after submit.

    namespace Microsoft.Restier.Samples.Northwind.Models
    {
        public class NorthwindApi : EntityFrameworkApi<NorthwindContext>
        {
            ...
            // Gets called before updating an entity from the entity set Products.
            protected void OnUpdatingProducts(Product product)
            {
                WriteLog(DateTime.Now.ToString() + product.ProductID + " is being updated");
            }
    
            // Gets called after inserting an entity to the entity set Products.
            protected void OnInsertedProducts(Product product)
            {
                WriteLog(DateTime.Now.ToString() + product.ProductID + " has been inserted");
            }
        }
    }
  • 2.4 Model Building [0.4.0 only]

    This section is for version 0.4.0 only, for newer version,refer to following section. RESTier supports various ways to build EDM model. Users may first get an initial model from the EF provider. Then RESTier’s ConventionBasedApiModelBuilder can further extend the model with additional entity sets, singletons and operations from the public properties and methods defined in the Api class. This subsection mainly talks about how to build an initial EDM model and then the convention RESTier adopts to extend an EDM model from an Api class.

    Build an initial EDM model

    The ConventionBasedApiModelBuilder requires EDM types to be present in the initial model because it is only responsible for building entity sets, singletons and operations NOT types. So anyway users need to build an initial EDM model with adequate types added in advance. The typical way to do so is to write a custom model builder implementing IModelBuilder and register it to the Api class. Here is an example using the ConventionModelBuilder in OData Web API to build an initial model only containing the Person type.

    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web.OData.Builder;
    using Microsoft.OData.Edm;
    using Microsoft.Restier.Core;
    using Microsoft.Restier.Core.Model;
    
    namespace Microsoft.Restier.WebApi.Test.Services.TrippinInMemory
    {
        public class TrippinApi : ApiBase
        {
            protected override ApiConfiguration CreateApiConfiguration()
            {
                return base.CreateApiConfiguration()
                    .AddHookHandler<IModelBuilder>(new ModelBuilder());
            }
    
            private class ModelBuilder : IModelBuilder
            {
                public Task<IEdmModel> GetModelAsync(InvocationContext context, CancellationToken cancellationToken)
                {
                    var builder = new ODataConventionModelBuilder();
                    builder.EntityType<Person>();
                    return Task.FromResult(builder.GetEdmModel());
                }
            }
        }
    }

    If RESTier entity framework provider is used and user has no additional types other than those in the database schema, no custom model builder or even the Api class is required because the provider will take over to build the model instead. But what the provider does behind the scene is similar.

    Extend a model from Api class

    The ConventionBasedApiModelBuilder will further extend the EDM model passed in using the public properties and methods defined in the Api class. Please note that all properties and methods declared in the parent classes are NOT considered.

    Entity set If a property declared in the Api class satisfies the following conditions, an entity set whose name is the property name will be added into the model.

    • Public
    • Has getter
    • Either static or instance
    • There is no existing entity set with the same name
    • Return type must be IQueryable<T> where T is class type

    Example:

    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.Restier.Core.Model;
    using Microsoft.Restier.Provider.EntityFramework;
    using Microsoft.Restier.WebApi.Test.Services.Trippin.Models;
    
    namespace Microsoft.Restier.WebApi.Test.Services.Trippin.Api
    {
        public class TrippinApi : DbApi<TrippinModel>
        {
            public IQueryable<Person> PeopleWithFriends
            {
                get { return Context.People.Include("Friends"); }
            }
            ...
        }
    }


    Singleton If a property declared in the Api class satisfies the following conditions, a singleton whose name is the property name will be added into the model.

    • Public
    • Has getter
    • Either static or instance
    • There is no existing singleton with the same name
    • Return type must be non-generic class type

    Example:

    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.Restier.Core.Model;
    using Microsoft.Restier.EntityFramework;
    using Microsoft.Restier.WebApi.Test.Services.Trippin.Models;
    
    namespace Microsoft.Restier.WebApi.Test.Services.Trippin.Api
    {
        public class TrippinApi : DbApi<TrippinModel>
        {
            ...
            public Person Me { get { return DbContext.People.Find(1); } }
            ...
        }
    }

    For versions under 0.4.0-beta, users must define an action with ODataRouteAttribute in their custom controller to access a singleton. After version 0.4.0-rc, no custom route is required. However due to some limitations from Entity Framework and OData spec, CUD (insertion, update and deletion) on the singleton entity are NOT supported directly by RESTier. Users need to define their own route to achieve these operations.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Web.Http;
    using System.Web.OData;
    using System.Web.OData.Extensions;
    using System.Web.OData.Routing;
    using Microsoft.OData.Edm.Library;
    using Microsoft.Restier.WebApi.Test.Services.Trippin.Api;
    using Microsoft.Restier.WebApi.Test.Services.Trippin.Models;
    
    namespace Microsoft.Restier.WebApi.Test.Services.Trippin.Controllers
    {
        public class TrippinController : ODataController
        {
            ...
            // Only needed <=0.4.0-beta
            [EnableQuery]
            [HttpGet]
            [ODataRoute("Me")]
            public IHttpActionResult Me()
            {
                return Ok(DbContext.People.Find(1));
            }
            ...
        }
    }


    Navigation property binding Starting from version 0.4.0-rc, the ConventionBasedApiModelBuilder follows the rules below to add navigation property bindings after entity sets and singletons have been built.

    • Bindings will ONLY be added for those entity sets and singletons that have been built inside ConventionBasedApiModelBuilder. Example: Entity sets built by the RESTier’s EF provider are assumed to have their navigation property bindings added already.
    • The ConventionBasedApiModelBuilder only searches navigation sources who have the same entity type as the source navigation property. Example: If the type of a navigation property is Person or Collection(Person), only those entity sets and singletons of type Person are searched.
    • Singleton navigation properties can be bound to either entity sets or singletons. Example: If Person.BestFriend is a singleton navigation property, bindings from BestFriend to an entity set People or to a singleton Boss are all allowed.
    • Collection navigation properties can ONLY be bound to entity sets. Example: If Person.Friends is a collection navigation property. ONLY binding from Friends to an entity set People is allowed. Binding from Friends to a singleton Boss is NOT allowed.
    • If there is any ambiguity among entity sets or singletons, no binding will be added. Example: For the singleton navigation property Person.BestFriend, no binding will be added if 1) there are at least two entity sets (or singletons) both of type Person; 2) there is at least one entity set and one singleton both of type Person. However for the collection navigation property Person.Friends, no binding will be added only if there are at least two entity sets both of type Person. One entity set and one singleton both of type Person will NOT lead to any ambiguity and one binding to the entity set will be added.

    If any expected navigation property binding is not added by RESTier, users can always manually add it through custom model extension (mentioned below).

    Operation If a method declared in the Api class satisfies the following conditions, an operation whose name is the method name will be added into the model.

    • Public
    • Either static or instance
    • There is no existing operation with the same name

    Example (namespace should be specified if the namespace of the method does not match the model):

    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.Restier.Core.Model;
    using Microsoft.Restier.EntityFramework;
    using Microsoft.Restier.WebApi.Test.Services.Trippin.Models;
    
    namespace Microsoft.Restier.WebApi.Test.Services.Trippin.Api
    {
        public class TrippinApi : DbApi<TrippinModel>
        {
            ...
            // Action import
            [Action(Namespace = "Microsoft.Restier.WebApi.Test.Services.Trippin.Models")]
            public void CleanUpExpiredTrips() {}
            
            // Bound action
            [Action(Namespace = "Microsoft.Restier.WebApi.Test.Services.Trippin.Models")]
            public Trip EndTrip(Trip bindingParameter) { ... }
            
            // Function import
            [Function(Namespace = "Microsoft.Restier.WebApi.Test.Services.Trippin.Models")]
            public IEnumerable<Person> GetPeopleWithFriendsAtLeast(int n) { ... }
            
            // Bound function
            [Function(Namespace = "Microsoft.Restier.WebApi.Test.Services.Trippin.Models")]
            public Person GetPersonWithMostFriends(IEnumerable<Person> bindingParameter) { ... }
            ...
        }
    }

    Please note that in order to access an operation user must define an action with ODataRouteAttribute in his custom controller.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Web.Http;
    using System.Web.OData;
    using System.Web.OData.Extensions;
    using System.Web.OData.Routing;
    using Microsoft.OData.Edm.Library;
    using Microsoft.Restier.WebApi.Test.Services.Trippin.Api;
    using Microsoft.Restier.WebApi.Test.Services.Trippin.Models;
    
    namespace Microsoft.Restier.WebApi.Test.Services.Trippin.Controllers
    {
        public class TrippinController : ODataController
        {
            private TrippinApi api;
            
            private TrippinApi Api
            {
                get
                {
                    if (api == null)
                    {
                        api = new TrippinApi();
                    }
                    
                    return api;
                }
            }
            ...
            [ODataRoute("Trips({key})/Microsoft.Restier.WebApi.Test.Services.Trippin.Models.EndTrip")]
            public IHttpActionResult EndTrip(int key)
            {
                var trip = DbContext.Trips.SingleOrDefault(t => t.TripId == key);
                return Ok(Api.EndTrip(trip));
            }
            ...
        }
    }

    Custom model extension

    If users have the need to extend the model even after RESTier’s conventions have been applied, ApiConfiguratorAttribute can be used. First implement a custom ApiConfiguratorAttribute and register a model extender in it. The difference from the previous TrippinApi.ModelBuilder is that the previous one does NOT need to implement IDelegateHookHandler<IModelBuilder> which provides it with the capability to call an inner model builder. The previous one itself is responsible for producing an initial model. However TrippinAttribute.TrippinModelExtender MUST implement this interface and call the inner model builder to at least get a workable model to extend. Notably the built-in ConventionBasedApiModelBuilder and ConventionBasedOperationProvider also follow this pattern.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.OData.Edm;
    using Microsoft.Restier.Core;
    using Microsoft.Restier.Core.Model;
    using Microsoft.Restier.EntityFramework;
    using Microsoft.Restier.WebApi.Test.Services.Trippin.Models;
    
    namespace Microsoft.Restier.WebApi.Test.Services.Trippin.Api
    {
        public class TrippinAttribute : ApiConfiguratorAttribute
        {
            public override void Configure(ApiConfiguration configuration, Type type)
            {
                // Add your custom model extender here.
                configuration.AddHookHandler<IModelBuilder>(new TrippinModelExtender());
            }
    
            private class TrippinModelExtender : IModelBuilder, IDelegateHookHandler<IModelBuilder>
            {
                public IModelBuilder InnerHandler { get; set; }
    
                public async Task<IEdmModel> GetModelAsync(InvocationContext context, CancellationToken cancellationToken)
                {
                    IEdmModel model = null;
                    
                    // Call inner model builder to get a model to extend.
                    if (this.InnerHandler != null)
                    {
                        model = await this.InnerHandler.GetModelAsync(context, cancellationToken);
                    }
    
                    // Do sth to extend the model such as add custom navigation property binding.
    
                    return model;
                }
            }
        }
    }

    Then apply it to the Api class.

    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.Restier.Core.Model;
    using Microsoft.Restier.EntityFramework;
    using Microsoft.Restier.WebApi.Test.Services.Trippin.Models;
    
    namespace Microsoft.Restier.WebApi.Test.Services.Trippin.Api
    {
        [Trippin]
        public class TrippinApi : ApiBase
        {
            ...
        }
    }

    After the above steps, the final process of building the model will be:

    • User’s model builder or RESTier provider’s model builder registered in CreateApiConfiguration: produce an initial model In this case: TrippinApi.ModelBuilder or Microsoft.Restier.EntityFramework.Model.ModelProducer.
    • ConventionBasedApiModelBuilder: extend the model with entity sets and singletons from Api class
    • ConventionBasedOperationProvider: extend the model with actions and functions from Api class
    • User’s model extender registered in custom ApiConfiguratorAttribute: custom model extension In this case: TrippinAttribute.TrippinModelExtender.

    Actually this order not only applies to the IModelBuilder but also all other hook handlers. The typical order for executing a hook handler will be:

    • Hook handlers registered in CreateApiConfiguration: provide an initial result
    • Hook handlers provided by RESTier conventions: apply RESTier conventions to the result
    • Hook handlers registered in custom ApiConfiguratorAttribute: user customizations
  • 2.5 Model Building [>=0.5.0]

    RESTier supports various ways to build EDM model. Users may first get an initial model from the EF provider. Then RESTier’s RestierModelExtender can further extend the model with additional entity sets, singletons and operations from the public properties and methods defined in the Api class. This subsection mainly talks about how to build an initial EDM model and then the convention RESTier adopts to extend an EDM model from an Api class.

    Build an initial EDM model

    The RestierModelExtender requires EDM types to be present in the initial model because it is only responsible for building entity sets, singletons and operations NOT types. So anyway users need to build an initial EDM model with adequate types added in advance. The typical way to do so is to write a custom model builder implementing IModelBuilder and register it to the Api class. Here is an example using the **ODataConventionModelBuilder** in OData Web API to build an initial model only containing the Person type. Any model building methods supported by Web API OData can be used here, refer to Web API OData Model builder document for more information.

    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web.OData.Builder;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.OData.Edm;
    using Microsoft.Restier.Core;
    using Microsoft.Restier.Core.Model;
    
    namespace Microsoft.OData.Service.Sample.TrippinInMemory
    {
        public class TrippinApi : ApiBase
        {
            protected override IServiceCollection ConfigureApi(IServiceCollection services)
            {
                services.AddService<IModelBuilder, CustomizedModelBuilder>();
                return base.ConfigureApi(services);
            }
    
            private class CustomizedModelBuilder : IModelBuilder
            {
                public Task<IEdmModel> GetModelAsync(InvocationContext context, CancellationToken cancellationToken)
                {
                    var builder = new ODataConventionModelBuilder();
                    builder.EntityType<Person>();
                    return Task.FromResult(builder.GetEdmModel());
                }
            }
        }
    }

    If RESTier entity framework provider is used and user has no additional types other than those in the database schema, no custom model builder or even the Api class is required because the provider will take over to build the model instead. But what the provider does behind the scene is similar. With entity framework provider, the model by default is built with ODataConventionModelBuilder, refer to document on the conversions been used like how the builder identifies keys for entity type and so on.

    Extend a model from Api class

    The RestierModelExtender will further extend the EDM model passed in using the public properties and methods defined in the Api class. Please note that all properties and methods declared in the parent classes are NOT considered.

    Entity set If a property declared in the Api class satisfies the following conditions, an entity set whose name is the property name will be added into the model.

    • Public
    • Has getter
    • Either static or instance
    • There is no existing entity set with the same name
    • Return type must be IQueryable<T> where T is class type

    Example:

    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.Restier.Core.Model;
    using Microsoft.Restier.Provider.EntityFramework;
    using Microsoft.OData.Service.Sample.Trippin.Models;
    
    namespace Microsoft.OData.Service.Sample.Trippin.Api
    {
        public class TrippinApi : EntityFrameworkApi<TrippinModel>
        {
            public IQueryable<Person> PeopleWithFriends
            {
                get { return Context.People.Include("Friends"); }
            }
            ...
        }
    }


    Singleton If a property declared in the Api class satisfies the following conditions, a singleton whose name is the property name will be added into the model.

    • Public
    • Has getter
    • Either static or instance
    • There is no existing singleton with the same name
    • Return type must be non-generic class type

    Example:

    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.Restier.Core.Model;
    using Microsoft.Restier.Provider.EntityFramework;
    using Microsoft.OData.Service.Sample.Trippin.Models;
    
    namespace Microsoft.OData.Service.Sample.Trippin.Api
    {
        public class TrippinApi : EntityFrameworkApi<TrippinModel>
        {
            ...
            public Person Me { get { return DbContext.People.Find(1); } }
            ...
        }
    }

    Due to some limitations from Entity Framework and OData spec, CUD (insertion, update and deletion) on the singleton entity are NOT supported directly by RESTier. Users need to define their own route to achieve these operations.

    Navigation property binding Starting from version 0.5.0, the RestierModelExtender follows the rules below to add navigation property bindings after entity sets and singletons have been built.

    • Bindings will ONLY be added for those entity sets and singletons that have been built inside RestierModelExtender. Example: Entity sets built by the RESTier’s EF provider are assumed to have their navigation property bindings added already.
    • The RestierModelExtender only searches navigation sources who have the same entity type as the source navigation property. Example: If the type of a navigation property is Person or Collection(Person), only those entity sets and singletons of type Person are searched.
    • Singleton navigation properties can be bound to either entity sets or singletons. Example: If Person.BestFriend is a singleton navigation property, bindings from BestFriend to an entity set People or to a singleton Boss are all allowed.
    • Collection navigation properties can ONLY be bound to entity sets. Example: If Person.Friends is a collection navigation property. ONLY binding from Friends to an entity set People is allowed. Binding from Friends to a singleton Boss is NOT allowed.
    • If there is any ambiguity among entity sets or singletons, no binding will be added. Example: For the singleton navigation property Person.BestFriend, no binding will be added if 1) there are at least two entity sets (or singletons) both of type Person; 2) there is at least one entity set and one singleton both of type Person. However for the collection navigation property Person.Friends, no binding will be added only if there are at least two entity sets both of type Person. One entity set and one singleton both of type Person will NOT lead to any ambiguity and one binding to the entity set will be added.

    If any expected navigation property binding is not added by RESTier, users can always manually add it through custom model extension (mentioned below).

    Operation If a method declared in the Api class satisfies the following conditions, an operation whose name is the method name will be added into the model.

    • Public
    • Either static or instance
    • There is no existing operation with the same name

    Example (namespace should be specified if the namespace of the method does not match the model):

    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.Restier.Core.Model;
    using Microsoft.Restier.Provider.EntityFramework;
    using Microsoft.OData.Service.Sample.Trippin.Models;
    
    namespace Microsoft.OData.Service.Sample.Trippin.Api
    {
        public class TrippinApi : EntityFrameworkApi<TrippinModel>
        {
            ...
            // Action import
            [Operation(Namespace = "Microsoft.OData.Service.Sample.Trippin.Models", HasSideEffects = true)]
            public void CleanUpExpiredTrips() {}
            
            // Bound action
            [Operation(Namespace = "Microsoft.OData.Service.Sample.Trippin.Models", HasSideEffects = true)]
            public Trip EndTrip(Trip bindingParameter) { ... }
            
            // Function import
            [Operation(Namespace = "Microsoft.OData.Service.Sample.Trippin.Models", EntitySet = "People")]
            public IEnumerable<Person> GetPeopleWithFriendsAtLeast(int n) { ... }
            
            // Bound function
            [Operation(Namespace = "Microsoft.OData.Service.Sample.Trippin.Models", EntitySet = "People")]
            public Person GetPersonWithMostFriends(IEnumerable<Person> bindingParameter) { ... }
            ...
        }
    }

    Note:

    1. Operation attribute’s EntitySet property is needed if there are more than one entity set of the entity type that is type of result defined. Take an example if two EntitySet People and AllPersons are defined whose entity type is Person, and the function returns Person or List of Person, then the Operation attribute for function must have EntitySet defined, or EntitySet property is optional.

    2. Function and Action uses the same attribute, and if the method is an action, must specify property HasSideEffects with value of true whose default value is false.

    3. In order to access an operation user must define an action with ODataRouteAttribute in his custom controller. Refer to section 3.3 for more information.

    Custom model extension

    If users have the need to extend the model even after RESTier’s conventions have been applied, user can use IServiceCollection AddService to add a ModelBuilder after calling base.ConfigureApi(services).

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.OData.Edm;
    using Microsoft.Restier.Core;
    using Microsoft.Restier.Core.Model;
    using Microsoft.Restier.Provider.EntityFramework;
    using Microsoft.OData.Service.Sample.Trippin.Models;
    
    namespace Microsoft.OData.Service.Sample.Trippin.Api
    {
        public class TrippinAttribute : ApiConfiguratorAttribute
        {
            protected override IServiceCollection ConfigureApi(IServiceCollection services)
            {
                services = base.ConfigureApi(services);
                // Add your custom model extender here.
                services.AddService<IModelBuilder, CustomizedModelBuilder>();
                return services;
            }
    
            private class CustomizedModelBuilder : IModelBuilder
            {
                public IModelBuilder InnerModelBuilder { get; set; }
    
                public async Task<IEdmModel> GetModelAsync(InvocationContext context, CancellationToken cancellationToken)
                {
                    IEdmModel model = null;
                    
                    // Call inner model builder to get a model to extend.
                    if (this.InnerModelBuilder != null)
                    {
                        model = await this.InnerModelBuilder.GetModelAsync(context, cancellationToken);
                    }
    
                    // Do sth to extend the model such as add custom navigation property binding.
    
                    return model;
                }
            }
        }
    }

    After the above steps, the final process of building the model will be:

    • User’s model builder registered before base.ConfigureApi(services) is called first.
    • RESTier’s model builder includes EF model builder and RestierModelExtender will be called.
    • User’s model builder registered after base.ConfigureApi(services) is called.

    If InnerModelBuilder method is not called first, then the calling sequence will be different. Actually this order not only applies to the IModelBuilder but also all other services.

    Refer to section 4.3 for more details of RESTier API Service.

  • 2.6 Composite Key [>=0.5.0]

    Composite key means one entity has more then one attributes for the key. It is automatically supported by RESTIer without any additional configurations.

    To request an entity with composite key, the URL will be like ~/EntitySet(keyName1=value1,keyName2=value2)

  • 2.7 Key As Segment [>=0.5.0]

    RESTier supports key as segment with one single line configuration before calling MapRestierRoute method:

    config.SetUrlConventions(ODataUrlConventions.ODataSimplified);

    Then request an entity with key as segment, the URL will be like ~/EntitySet/KeyValue

    Note : If entity type has composite key, then key as segment is not supported for this entity type.

  • 2.8 Customize Query [>=0.5.0]

    RESTier supports to customize the query setting and query process logic.

    1. Customize Query Setting

    RESTier supports to customize kinds of query setting like AllowedLogicalOperators, AllowedQueryOptions, MaxExpansionDepth, MaxAnyAllExpressionDepth and so on. Refer to class for full list of settings.

    This is an example on how to customize MaxExpansionDepth from default value 2 to 3 which means allowing two level nested expand now, refer to this link to see the end to end samples,

    First create a factory delegate which will create a new instance of ODataValidationSettings, then registering it into RESTier Dependency Injection framework as a service via overriding the ConfigureApi method in your Api class.

            protected override IServiceCollection ConfigureApi(IServiceCollection services)
            {
                // Add OData Query Settings and valiadtion settings
                Func<IServiceProvider, ODataValidationSettings> validationSettingFactory = (sp) => new ODataValidationSettings
                {
                    MaxAnyAllExpressionDepth =3,
                    MaxExpansionDepth = 3
                };
    
                return base.ConfigureApi(services)
                    .AddSingleton<ODataValidationSettings>(validationSettingFactory);
            }

    Then $expand with supported with max two nested $expand via only max one nested $expand is supported by default before we apply this customization.

    2. Customize Query Logic

    RESTier supports built in convention based query customized logic (refer to section 2.2), besides this, RESTier has two interfaces IQueryExpressionAuthorizer and IQueryExpressionProcessor for end user to further customize the query process logic.

    Customized Authorize Logic

    User can use interface IQueryExpressionAuthorizer to define any customize authorize logic to see whether user is authorized for the specified query, if this method returns false, then the related query will get error code 403 (forbidden).

    There are two steps to plug in customized process logic,

    First create a class CustomizedAuthorizer implement IQueryExpressionAuthorizer, and add any process logic needed.

    Second, registering it into RESTier Dependency Injection framework as a service via overriding the ConfigureApi method in your Api class.

            protected override IServiceCollection ConfigureApi(IServiceCollection services)
            {
    
                return base.ConfigureApi(services)
                    .AddService<IQueryExpressionInspector, CustomizedInspector>();
            }

    In CustomizedAuthorizer, user can decide whether to call the RESTier logic, if user decide to call the RESTier logic, user can defined a property like “private IQueryExpressionAuthorizer InnerAuthorizer {get; set;}” in class CustomizedAuthorizer, then call InnerAuthorizer.Authorize() to call RESTier logic.

    Customized Process Logic

    User can create class implementing interface IQueryExpressionProcessor to customize the LINQ query expression build process like to replace part of expression, remove part of expression or append part of expression. Then registered the customized class as DI service.The steps to plugin is same as above.

    The logic OnFilter[entity set name] is been processed by RESTier default expression processor which add a where clause after entity set. The way to call default logic is same as above.

  • 2.9 Customize Submit [>=0.5.0]

    RESTier supports built in convention based logic (refer to section 2.3) for submit, besides this, RESTier has three interfaces IChangeSetItemAuthorizer, IChangeSetItemValidator and IChangeSetItemProcessor for end user to customize the logic.

    Customized Authorize Logic

    User can use interface IChangeSetItemAuthorizer to define any customize authorize logic to see whether user is authorized for the specified submit, if this method return false, then the related query will get error code 403 (forbidden).

    There are two steps to plug in customized process logic,

    First create a class CustomizedAuthorizer implement IChangeSetItemAuthorizer, and add any process logic needed.

    public class CustomizedAuthorizer : IChangeSetItemAuthorizer
    {
        // The inner Authorizer will call CanUpdate/Insert/Delete<EntitySet> method
        private IChangeSetItemAuthorizer InnerAuthorizer { get; set; }
    
        public Task<bool> AuthorizeAsync(
            SubmitContext context,
            ChangeSetItem item,
            CancellationToken cancellationToken)
        {
    	    // Add any customized logic here
        }
    }

    Second, registering it into RESTier Dependency Injection framework as a service via overriding the ConfigureApi method in your Api class.

    namespace Microsoft.OData.Service.Sample.Trippin.Api
    {
        public class TrippinApi : EntityFrameworkApi<TrippinModel>
        {
            protected override IServiceCollection ConfigureApi(IServiceCollection services)
            {
                return base.ConfigureApi(services)
                    .AddService<IChangeSetItemAuthorizer, CustomizedAuthorizer>();
            }
        }
    }

    In CustomizedAuthorizer, user can decide whether to call the RESTier logic, if user decide to call the RESTier logic, user can defined a property like “private IChangeSetItemAuthorizer InnerAuthorizer {get; set;}” in class CustomizedAuthorizer, then call InnerAuthorizer.AuthorizeAsync() to call RESTier logic which call Authorize part logic defined in section 2.3.

    Customized Validation Logic

    User can use interface IChangeSetItemValidator to customize validation logic for submit, and if validate fails, add a error validation result to validation results, then the request will get 400(bad request) return code, here is a sample customize validation logic,

    public class CustomizedValidator : IChangeSetItemValidator
    {
    	// Add any customized validation into this method
        public Task ValidateChangeSetItemAsync(
            SubmitContext context,
            ChangeSetItem item,
            Collection<ChangeSetItemValidationResult> validationResults,
            CancellationToken cancellationToken)
        {
    	    DataModificationEntry dataModificationEntry = entry as DataModificationEntry;
    	    var entity = dataModificationEntry.Entity;
    	
    	     // Customized validate logic and if there is error, add a validation result with error level.
    	    validationResults.Add(new ChangeSetValidationResult()
    	    {
    	        Id = dataModificationEntry.EntitySetName+ dataModificationEntry.EntityKey,
    	        Message = "Customized error message",
    	        Severity = EventLevel.Error,
    	        Target = entity
    	    });
    	}
    }

    The steps to plugin the logic is same as above.

    Customized Process Logic

    User can use interface IChangeSetItemProcessor to customize logic before or after submit, OnProcessingChangeSetItemAsync logic is called before submit and OnProcessedChangeSetItemAsync logic is called after submit, RESTier default logic is defined in section 2.3 plugin user logic part. Default logic can be called via defined a property with type IChangeSetItemProcessor like “private IChangeSetItemProcessor InnerProcessor {get; set;}”, and user call InnerProcessor.OnProcessingChangeSetItemAsync or OnProcessedChangeSetItemAsync to call RESTier logic, if in CustomizedProcessor, there is no such property defined or InnerProcessor is not used, then RESTier logic will not be called.

    namespace Microsoft.OData.Service.Sample.Trippin.Submit
    {
        public class CustomizedSubmitProcessor : IChangeSetItemProcessor
        {
            private IChangeSetItemProcessor InnerProcessor { get; set; }
    
            // Any customized logic needed before persist called can be added here.
            // InnerProcessor call related OnUpdating|Inseting|Deleting<EntitySet> methods
            public Task OnProcessingChangeSetItemAsync(SubmitContext context, ChangeSetItem item, CancellationToken cancellationToken)
            {
                return InnerProcessor.OnProcessingChangeSetItemAsync(context, item, cancellationToken);
            }
    
            // Any customized logic needed after persist called can be added here.
            // InnerProcessor call related OnUpdated|Inseted|Deleted<EntitySet> methods
            public Task OnProcessedChangeSetItemAsync(SubmitContext context, ChangeSetItem item, CancellationToken cancellationToken)
            {
                var dataModificationItem = item as DataModificationItem;
                if (dataModificationItem != null)
                {
                    object myEntity = dataModificationItem.Entity;
                    string entitySetName = dataModificationItem.EntitySetName;
                    ChangeSetItemAction operation = dataModificationItem.ChangeSetItemAction;
    
                    // In case of insert, the request URL has no key, and request body may not have key neither as the key may be generated by database
                    var keyAttrbiutes = new Dictionary<string, object>();
                    var keyConvention = new Dictionary<string, object>();
    
                    var entityTypeName = myEntity.GetType().Name;
                    PropertyInfo[] properties = myEntity.GetType().GetProperties();
    
                    foreach (PropertyInfo property in properties)
                    {
                        var attribute = Attribute.GetCustomAttribute(property, typeof(KeyAttribute))
                            as KeyAttribute;
                        var propName = property.Name;
                        // This is getting key with Key attribute defined
                        if (attribute != null) // This property has a KeyAttribute
                        {
                            // Do something, to read from the property:
                            object val = property.GetValue(myEntity);
                            keyAttrbiutes.Add(propName, val);
                        }
                        // This is getting key based on convention
                        else if(propName.ToLower().Equals("id") || propName.ToLower().Equals(entityTypeName.ToLower()+"id"))
                        {
                            object val = property.GetValue(myEntity);
                            keyConvention.Add(propName, val);
                        }
                    }
                    if (keyAttrbiutes.Count > 0)
                    {
                        // Use property with key attribute as keys    
                    }
                    else if(keyConvention.Count > 0)
                    {
                        // Key is defined based on convention
                    }
                }
                return InnerProcessor.OnProcessedChangeSetItemAsync(context, item, cancellationToken);
            }
        }
    }

    The steps to plugin the logic is same as above.

  • 2.10 Customize Payload Converter [>=0.5.0]

    RESTier supports to customize the payload to be read or written (a.k.a serialize and deserialize), user can extend the class RestierPayloadValueConverter to overwrite method ConvertToPayloadValue for payload writing and ConvertFromPayloadValue for payload reading.

    This is an example on how to customize a specified string value to add some prefix and write into response, refer to this link to see the end to end samples,

    1. Create a class to have the customized converter logic

        public class CustomizedPayloadValueConverter : RestierPayloadValueConverter
        {
            public override object ConvertToPayloadValue(object value, IEdmTypeReference edmTypeReference)
            {
                if (edmTypeReference != null)
                {
                    if (value is string)
                    {
                        var stringValue = (string) value;
    
                        // Make a single string value "Russell" converted to have additional suffix
                        if (stringValue == "Russell")
                        {
                            return stringValue + "Converter";
                        }
                    }
                }
    
                return base.ConvertToPayloadValue(value, edmTypeReference);
            }
        }

    2. Register customized converter into RESTier Dependency Injection framework as a service via overriding the ConfigureApi method in your Api class.

            protected override IServiceCollection ConfigureApi(IServiceCollection services)
            {
                return base.ConfigureApi(services)
                    .AddSingleton<ODataPayloadValueConverter, CustomizedPayloadValueConverter>();
            }

    Then when writting payload for response, any string which has value “Russell” will become “RussellConverter”.

  • 2.11 Customize Serializer and Deserializer Provider [>=0.5.0]

    RESTier supports to customize serializer and deserializer provider for payload reading and writing, then in the provider, it can return customized serializer or deserializer for specified EdmType to customize the payload reading and writing.

    This is an example on how to customize ODataComplexTypeSerializer to customize how complex type payload is serialized for response.

    First create a class which extends ODataComplexTypeSerializer, and override method WriteObject.

    Second create a class which extends DefaultRestierSerializerProvider, and override method GetODataPayloadSerializer and GetEdmTypeSerializer which will return the customized serializer, and this is sample code,

            public override ODataSerializer GetODataPayloadSerializer(
                IEdmModel model,
                Type type,
                HttpRequestMessage request)
            {
                ODataSerializer serializer = null;
                if (type == typeof (ComplexResult))
                {
                    serializer = customizerComplexSerialier;
                }
                else
                {
                    serializer = base.GetODataPayloadSerializer(model, type, request);
                }
    
                return serializer;
            }
    
            public override ODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType)
            {
                if (edmType.IsEntity())
                {
                    return this.entityTypeSerializer;
                }
    
                if (edmType.IsComplex())
                {
                    return customizerComplexSerialier;
                }
    
                return base.GetEdmTypeSerializer(edmType);
    
            }

    Third, register customized serializer provider as DI service in the Api ConfigureApi method

            protected override IServiceCollection ConfigureApi(IServiceCollection services)
            {
                return base.ConfigureApi(services)
                    .AddSingleton<ODataSerializerProvider, CustomizedSerializerProvider>();
            }

    With these customized code, the complex result will be serialized in the customized way.

3. EXTENSIONS

  • 3.1 Use temporal types in RESTier Entity Framework [>=0.5.0]

    Restier.EF now supports various temporal types. Compared to the previous support, the current solution is more consistent and extensible. You can find the detailed type-mapping table among EF type, SQL type and EDM type from the comment in Issue #279. Now almost all the OData scenarios (CRUD) of these temporal types should be well supported by RESTier.

    This subsection shows how to use temporal types in Restier.EF.

    Add Edm.DateTimeOffset property

    Suppose you have an entity class Person, all the following code define Edm.DateTimeOffset properties in the EDM model though the underlying SQL types are different (see the value of the TypeName property). You can see Column attribute is optional here.

    using System;
    using System.ComponentModel.DataAnnotations.Schema;
    
    public class Person
    {
        public DateTime BirthDateTime1 { get; set; }
    
        [Column(TypeName = "DateTime")]
        public DateTime BirthDateTime2 { get; set; }
    
        [Column(TypeName = "DateTime2")]
        public DateTime BirthDateTime3 { get; set; }
    
        public DateTimeOffset BirthDateTime4 { get; set; }
    }

    Add Edm.Date property

    The following code define an Edm.Date property in the EDM model.

    using System;
    using System.ComponentModel.DataAnnotations.Schema;
    
    public class Person
    {
        [Column(TypeName = "Date")]
        public DateTime BirthDate { get; set; }
    }

    Add Edm.Duration property

    The following code define an Edm.Duration property in the EDM model.

    using System;
    using System.ComponentModel.DataAnnotations.Schema;
    
    public class Person
    {
        public TimeSpan WorkingHours { get; set; }
    }

    Add Edm.TimeOfDay property

    The following code define an Edm.TimeOfDay property in the EDM model. Please note that you MUST NOT omit the ColumnTypeAttribute on a TimeSpan property otherwise it will be recognized as an Edm.Duration as described above.

    using System;
    using System.ComponentModel.DataAnnotations.Schema;
    
    public class Person
    {
        [Column(TypeName = "Time")]
        public TimeSpan BirthTime { get; set; }
    }

    As before, if you have the need to override ODataPayloadValueConverter, please now change to override RestierPayloadValueConverter instead in order not to break the payload value conversion specialized for these temporal types.

  • 3.2 Use Controllers in RESTier [>=0.4.0]

    RESTier aims to achieve more OData features with less user code. Currently in OData Web API users have to write a controller for each entity set or singleton and a lot of actions in that controller to support various property access. Mostly code among controllers is similar and redundant. Thus RestierController (previously ODataDomainController) was introduced to serve as the globally unique controller to handle most OData requests. While most is not everything, there are a few scenarios not covered by RestierController yet. As a result, traditional controllers (ODataController or ApiController) are still supported in RESTier’s routing convention with higher priority than RestierController. With such a flexible design, RESTier can satisfy various user requirements to implement an OData service.

    OData features supported by RestierController

    Now users need not write any controller code any more to enjoy the following OData features provided by RestierController:

    • Query service document
    GET ~
    • Query metadata document
    GET ~/$metadata
    • Query entity set
    GET ~/People
    • Query single entity
    GET ~/People(1)
    • Query any property path
    GET ~/People(1)/FirstName (primitive property)
    GET ~/People(1)/FavoriteFeature (enum property)
    GET ~/People(1)/Friends (navigation property)
    GET ~/People(1)/Emails (collection property)
    GET ~/Events(1)/OccursAt (complex property)
    GET ~/Events(1)/OccursAt/Address
    • Query entity/value count (by $count)
    GET ~/People(1)/$count
    GET ~/People(1)/Friends/$count
    GET ~/People(1)/Emails/$count
    • Query raw property value (by $value)
    GET ~/People(1)/FirstName/$value
    GET ~/People(1)/FavoriteFeature/$value
    GET ~/Events(1)/OccursAt/Address/$value
    • Create an entity
    POST ~/People
    • Fully update an entity
    PUT ~/People(1)
    • Partially update an entity
    PATCH ~/People(1)
    • Delete an entity
    DELETE ~/People(1)

    A little secret behind query

    Users may wonder how RESTier handles all these queries in a generic way in only one controller. Actually RestierController will use an internal class RestierQueryBuilder to go through each ODataPathSegment and gradually compose a LINQ query. Here is an example. If user sends the following query:

    GET ~/People(1)/Emails/$count

    The final LINQ query generated will be like (suppose EF is being used):

    DbContext.People.Where<Person>(p => p.PersonId == 1).SelectMany<string>(p => p.Emails).Count();

    Use custom controllers

    Users may not always want their requests to be processed by RestierController. RESTier of course provides several ways to override this.

    • Convention routing. If user defines a controller (MUST inherit from ODataController) with specific name for an entity set (like PeopleController for the entity set People), all requests to that entity set will be routed to the the user-defined controller instead of RestierController. Refer to convention routing document for more details.
    • Attribute routing. ODataRouteAttribute always has the highest priority in routing. Now users are recommended to use attribute routing to implement OData operation and singleton. Refer to attribute routing document for more details.
  • 3.3 Operations [>=0.4.0]

    To supports operation, there are two major items, first being able to build model for operation, refer to section 2.5 for more details. Second support to route operation requests to a Web API controller class action, this is covered in this section.

    Currently RESTier can not route an operation request to a method defined in API class for operation model building, user need to define its own controller with ODataRoute attribute for operation route.

    Operation includes function (bounded), function import (unbounded), action (bounded), and action(unbounded).

    For function and action, the ODataRoute attribute must include namespace information. There is a way to simplify the URL to omit the namespace, user can enable this via call “config.EnableUnqualifiedNameCall(true);” during registering.

    For function import and action import, the ODataRoute attribute must NOT include namespace information.

    RESTier also supports operation request in batch request, as long as user defines its own controller for operation route.

    This is an example on how to define customized controller with ODataRoute attribute for operation.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Web.Http;
    using System.Web.OData;
    using System.Web.OData.Extensions;
    using System.Web.OData.Routing;
    using Microsoft.OData.Edm.Library;
    using Microsoft.OData.Service.Sample.Trippin.Api;
    using Microsoft.OData.Service.Sample.Trippin.Models;
    
    namespace Microsoft.OData.Service.Sample.Trippin.Controllers
    {
        public class TrippinController : ODataController
        {        
            private TrippinApi Api
            {
                get
                {
                    if (api == null)
                    {
                        api = new TrippinApi();
                    }
                    
                    return api;
                }
            }
            ...
            // Unbounded action does not need namespace in route attribute
            [ODataRoute("ResetDataSource")]
            public IHttpActionResult ResetDataSource()
            {
                // reset the data source;
                return StatusCode(HttpStatusCode.NoContent);
            }
    
            [ODataRoute("Trips({key})/Microsoft.OData.Service.Sample.Trippin.Models.EndTrip")]
            public IHttpActionResult EndTrip(int key)
            {
                var trip = DbContext.Trips.SingleOrDefault(t => t.TripId == key);
                return Ok(Api.EndTrip(trip));
            }
            ...
        }
    }
  • 3.4 In-Memory Provider [0.4.0 only]

    RESTier supports building an OData service with all-in-memory resources. However currently RESTier has not provided a dedicated in-memory provider (probably part of the future plan) module so users have to write some service code to bootstrap the initial model with EDM types themselves. There is a sample service with in-memory provider here. This subsection mainly talks about how such a service is created.

    First please create an Empty ASP.NET Web API project following the instructions in Section 1.2. Stop BEFORE the Generate the model classes part.

    Create the Api class

    Create a simple data type Person with some properties and “fabricate” some fake data. Then add the first entity set People to the Api class:

    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web.OData.Builder;
    using Microsoft.OData.Edm;
    using Microsoft.Restier.Core;
    using Microsoft.Restier.Core.Model;
    
    namespace Microsoft.Restier.WebApi.Test.Services.TrippinInMemory
    {
        public class TrippinApi : ApiBase
        {
            private static readonly List<Person> people = new List<Person>
            {
                ...
            };
    
            public IQueryable<Person> People
            {
                get { return people.AsQueryable(); }
            }
        }
    }

    Create an initial model

    Since the RESTier convention will not produce any EDM type, an initial model with at least the Person type needs to be created by service. Here the ODataConventionModelBuilder from OData Web API is used for quick model building.

    namespace Microsoft.Restier.WebApi.Test.Services.TrippinInMemory
    {
        public class TrippinApi : ApiBase
        {
            protected override ApiConfiguration CreateApiConfiguration()
            {
                return base.CreateApiConfiguration()
                    .AddHookHandler<IModelBuilder>(new ModelBuilder());
            }
    
            private class ModelBuilder : IModelBuilder
            {
                public Task<IEdmModel> GetModelAsync(InvocationContext context, CancellationToken cancellationToken)
                {
                    var builder = new ODataConventionModelBuilder();
                    builder.EntityType<Person>();
                    return Task.FromResult(builder.GetEdmModel());
                }
            }
        }
    }

    Configure the OData endpoint

    Replace the WebApiConfig class with the following code. No need to create a custom controller if users don’t have attribute routing.

    using System.Web.Http;
    using Microsoft.Restier.WebApi.Batch;
    
    namespace Microsoft.Restier.WebApi.Test.Services.TrippinInMemory
    {
        public static class WebApiConfig
        {
            public static void Register(HttpConfiguration config)
            {
                config.MapRestierRoute<TrippinApi>(
                    "TrippinApi",
                    "api/Trippin",
                    new RestierBatchHandler(GlobalConfiguration.DefaultServer)).Wait();
            }
        }
    }
  • 3.5 In-Memory Provider [>=0.5.0]

    RESTier supports building an OData service with all-in-memory resources. However currently RESTier has not provided a dedicated in-memory provider module so users have to write some service code to bootstrap the initial model with EDM types themselves. There is a sample service with in-memory provider here. This subsection mainly talks about how such a service is created.

    First please create an Empty ASP.NET Web API project following the instructions in Section 1.2. Stop BEFORE the Generate the model classes part.

    Create the Api class

    Create a simple data type Person with some properties and “fabricate” some fake data. Then add the first entity set People to the Api class:

    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web.OData.Builder;
    using Microsoft.OData.Edm;
    using Microsoft.Restier.Core;
    using Microsoft.Restier.Core.Model;
    
    namespace Microsoft.OData.Service.Sample.TrippinInMemory
    {
        public class TrippinApi : ApiBase
        {
            private static readonly List<Person> people = new List<Person>
            {
                ...
            };
    
            public IQueryable<Person> People
            {
                get { return people.AsQueryable(); }
            }
        }
    }

    Create an initial model

    Since the RESTier convention will not produce any EDM type, an initial model with at least the Person type needs to be created by service. Here the ODataConventionModelBuilder from OData Web API is used for quick model building. Any model building methods supported by Web API OData can be used here, refer to Web API OData Model builder document for more information.

    namespace Microsoft.OData.Service.Sample.TrippinInMemory
    {
        public class TrippinApi : ApiBase
        {
            protected override IServiceCollection ConfigureApi(IServiceCollection services)
            {
                services.AddService<IModelBuilder>(new ModelBuilder());
                return base.ConfigureApi(services);
            }
    
            private class ModelBuilder : IModelBuilder
            {
                public Task<IEdmModel> GetModelAsync(InvocationContext context, CancellationToken cancellationToken)
                {
                    var builder = new ODataConventionModelBuilder();
                    builder.EntityType<Person>();
                    return Task.FromResult(builder.GetEdmModel());
                }
            }
        }
    }

    Configure the OData endpoint

    Replace the WebApiConfig class with the following code. No need to create a custom controller if users don’t have attribute routing.

    using System.Web.Http;
    using Microsoft.Restier.Publisher.OData.Batch;
    
    namespace Microsoft.OData.Service.Sample.TrippinInMemory
    {
        public static class WebApiConfig
        {
            public static void Register(HttpConfiguration config)
            {
                config.MapRestierRoute<TrippinApi>(
                    "TrippinApi",
                    "api/Trippin",
                    new RestierBatchHandler(GlobalConfiguration.DefaultServer)).Wait();
            }
        }
    }

4. DEEP IN RESTIER

  • 4.1 RESTier infrastructure

    Restier provides a connection between various data sources and existing clients. The framework contains 4 components: Core, Module, Provider and Publisher:

    • The core component provides functionalities for building up domain specific metadata, and logic for data CRUD processing flow. It also includes some extensible interfaces which allows pluggable modules.
    • The module component provides the common service elements such as authorization, logging, and conventions that allow users to set up a service more quickly.
    • The provider component includes data source adapters which provide functionalities for building up metadata and conduct data exchange with external data sources.
    • The publisher component provides functionalities for exposing the domain specific data via a new service interface, which could be understand by existing clients.
  • 4.2 RESTier Hook Handler (0.4.0 only)

    The hook handler in RESTier provides a way for setting custom extension points.

    Implement a single hook handler

    Basically we have the following interface for all hook handlers

    public interface IHookHandler
    {
    }

    For a specific hook handler, we will also provide the corresponding interface. For example, for model builder, we have the following interface hook handler for building up a service model:

    public interface IModelBuilder : IHookHandler
    {
     
    	Task<IEdmModel> GetModelAsync(InvocationContext context, CancellationToken cancellationToken);
    }

    To support this hook handler, user could write his custom class and implement such interface.

    Implement a chained hook handler

    Before 0.3.0-beta2, RESTier supports two kinds of hook (singleton hook and multi-cast hook point handler). For singleton hook, users can specifiy one single hook implementation, and the invoker will call the hook once. For multi-cast hook, users can add the hook implementation in sequence, while the invoker will call all the hooks in certain order (it may vary for different hook.)

    But later we introduced the current hook mechanism, which allows the user to maintain the hook chain. This is also something called Matryoshka doll model.

    If user wants to support hook hanlder chain, he could choose to implement the following interface besides the IHookHandler.

    public interface IDelegateHookHandler<T> where T : IHookHandler
    {
    	
    	T InnerHandler { get; set; }
    }

    For example, to support a chain of model producer, we can have the following class:

    public class MyModelBuilder : IModelBuilder, IDelegateHookHandler<IModelBuilder>
    {
    	public IModelBuilder InnerHandler { get; set; }
    	
    	public async Task<IEdmModel> GetModelAsync(InvocationContext context, CancellationToken cancellationToken)
    	{
    		EdmModel model = null;
    		if (this.InnerHandler != null)
    		{
    			model = await this.InnerHandler.GetModelAsync(context, cancellationToken) as EdmModel;
    		}
    
    		if (model == null)
    		{
    			// We don't plan to extend an empty model with operations.
    			return null;
    		}
    
    		// some model extender
    		return model;
    	}
    }

    The InnnerHandler would be auto set to a previous hook handler if present, during the time this hook handler was added.

    Set a hook handler

    Then we have got the following two APIs for setting hook handlers:

    public class ApiConfiguration 
    {
    	public ApiConfiguration AddHookHandler<T>(T handler) where T : class, IHookHandler
    }

    For each Api class instance, it will have a corresponding configuration. And we can set the hook handlers on the ApiConfiguration instance.

  • 4.3 RESTier API Service (>=0.5.0)

    Users can inject their custom API services into RESTier to extend various functionalities. There is a big progress since 0.4.0. Now the concept of hook handler has become API service in 0.5.0. We have removed the old interfaces IHookHandler and IDelegateHookHandler to adapt to the concept change. Thus the implementation of any custom API service (previously known as hook handler) should also be changed accordingly.

    All API services registered as one specific type (either a class or an interface) are organized in a consistently chained (or nested) way. Each API service in a chain can choose whether to call the next (or inner) API service. The last API service registered is always invoked first if current service always call inner method first.

    As a practical example in RESTier, there is an API service interface called IModelBuilder to build or extend an EDM model. By default, RESTier will register two model builders for IModelBuilder. The model builder from the data provider (e.g., ModelProducer in RESTier EF) is always registered first. The RestierModelExtender is always registered later. Any custom model builder will be registered sequentially between or before or after the two built-in model builders based on the way been registered. When the API service IModelBuilder is invoked, the outermost ModelBuilder is always invoked first. It first invokes the inner API service which could possibly be the model builder from the data provider or some custom model builder (if any). The custom model builder can choose to extend the model returned from an inner builder, or otherwise it can simply choose not to call the inner one and directly return a new model. The model builder from the data provider is typically innermost and thus has no inner builder to call.

    This subsection shows how to implement custom API services and inject them into RESTier between two built-in model builders. For before or after, refer to section 2.5 Model Building part. This is common for all other services which allows customization.

    Implement an API service

    The following sample code is to implement a custom model builder. Please note that if you want to call the inner builder, you need to put a settable property of IModelBuilder into your builder class. The accessibility and the name of the property doesn’t matter here. Then try to call the inner builder in the service implementation. If you don’t want to call any inner builder, you can just omit the property and remove the related logic.

    public class MyModelBuilder : IModelBuilder
    {
        // This is only needed if you want to call inner (or next) model builder logic in the way of chain 
        public IModelBuilder InnerBuilder { get; set; }
    
        public async Task<IEdmModel> GetModelAsync(InvocationContext context, CancellationToken cancellationToken)
        {
            IEdmModel model = null;
            if (this.InnerBuilder != null)
            {
                // Call the inner builder to build a model first.
                model = await this.InnerBuilder.GetModelAsync(context, cancellationToken);
            }
            
            if (model != null)
            {
                // Do something to extend the model.
            }
    
            return model;
        }
    }

    Register an API service

    We need to register MyModelBuilder into the API to make it work. You can override the ConfigureApi method in your API class to do so. Here is the sample code. There are also overloads for the two methods that take an existing service instance or a service factory method. By the way, all those methods are fluent API so you can call them in a chained way.

    public class MyApi : ApiBase
    {
        protected override IServiceCollection ConfigureApi(IServiceCollection services)
        {        
                Type apiType = this.GetType();
                // Add core and convention's services
                services = services.AddCoreServices(apiType)
                    .AddAttributeServices(apiType)
                    .AddConventionBasedServices(apiType);
    
                // Add EF related services which has ModelProducer
                services.AddEfProviderServices<NorthwindContext>();
    
                // Add customized services, after EF model builder and before WebApi operation model builder
                services.AddService<IModelBuilder, MyModelBuilder>();
    
                // This is used to add the publisher's services which has RestierModelExtender
                ApiConfiguration.GetPublisherServiceCallback(apiType)(services);
    
                return services;
        }
    }

    In the service implementation, the parameter type IServiceCollection is actually a container builder from Microsoft Dependency Injection Framework (DI). You can do whatever applicable to a normal DI container here. It is notable that you can also take advantage of the powerful scope feature in DI here! RESTier will create a new scope for each individual request in ApiContext which enables you to register scoped services whose lifetime is per-request.

    Please visit https://docs.asp.net/en/latest/fundamentals/dependency-injection.html to grasp some basic understanding about DI before proceeding.

    The following example is to register a scoped MyDbContext service so that you have a new MyDbContext instance for each request.

    public class MyDbContext : DbContext {...}
    
    public class MyApi : ApiBase
    {
        protected override IServiceCollection ConfigureApi(IServiceCollection services)
        {
            return base.ConfigureApi(services)
                .AddScoped<MyDbContext>(sp => sp.GetService<T>());
        }
    }

    You can also make a specific API service singleton, scoped or transient (though not common) by calling MakeSingleton, MakeScoped or MakeTransient. Here is a sample which is to make IModelBuilder scoped.

    public class MyApi : ApiBase
    {
        protected override IServiceCollection ConfigureApi(IServiceCollection services)
        {
            // Previous registered logic
            ...
            services.MakeScoped<IModelBuilder>();
            return services;
        }
    }

    Get an API service

    No matter in which way you register an API service of T, the only and unified way to get that service out is to use (ApiContext).GetApiService<T> from RESTier or IServiceProvider.GetService<T> from DI.

5. CLIENTS

6. ANNOUNCEMENTS

  • 6.1 Release notes for RESTier 0.2.0-preview

    Below are the features supported in the RESTier 0.2.0-preview, as well as the limitations of the current version.

    Easily build an OData V4 service

    Features directly supported

    Just create one ODataDomainController<> and all of the features below are automatically enabled:

    • Basic queries for metadata and top level entities.
    • System query options $select, $expand, $filter, $orderby, $top, $skip, and $format.
    • Ability to request related entities.
    • Create, Update and Delete top-level entities.
    • Batch requests.

    Leverage attribute routing to fall back to Web API OData for features not directly supported by RESTier

    • Request entity references with $ref.
    • Create, Update and Delete entities not on the top-level.
    • Modify relationships between entities.
    • etc.

    Use EdmModelExtender to support features currently not directly supported by RESTier.

    • OData functions.
    • OData actions

    Rich domain logic

    • Role-based security

      You can easily set restrictions for different entity sets. For example, you can provide users with READ permission on some entity sets, and INSPECT (only provides access to $metadata) on others.

    • Imperative views

      Customized entity sets which are not in the data model can be easily added. Currently, these entity sets are read-only, and do not support CUD (Create, Update, Delete) operations.

    • Entity set filters

      With entity set filters, you can easily set filters before entity data is retrieved. For example, if you want users to only see part of Customers based on their UserID, you can use entity set filters to pre-filter the results.

    • Submit logic

      With submit logic, you can add custom business logic that fires during or after a specific operation is performed on an entity set (e.g., OnInsertedProducts).

    Limitations

    • Only supports OData V4.
    • Only supports Entity Framework as data providers.

    These are the two primary limitations currently, and we are looking at mitigating them in future releases. In the meanwhile, we’d like to hear your feedback and suggestions on how to improve RESTier.

  • 6.2 RESTier now open sourced on GitHub

    The source code of RESTier now is open-souced on GitHub, together with the test code and the Northwind Samples.

    We have heard a lot of feedback of RESTier and record them directly on GitHub Issues, with the source code open now, developers can explore and play with RESTier more easily. And code contributions, bug reports are warmly welcomed.

  • 6.3 Release notes for RESTier 0.3.0-beta1

    Features supported in 0.3.0-beta1

    • Complex type support #96


    Improvements since 0.2.0-pre

    • Northwind service uses script to generate database instead of .mdf/.ldf files #77
    • Add StyleCop and FxCop to build process to ensure code quality
    • TripPin service supports singleton
    • Visual Studio 2015 and MSSQLLocalDB
    • Use xUnit 2.0 as the test framework for RESTier #104
  • 6.4 Release notes for RESTier 0.3.0-beta2

    Features supported in 0.3.0-beta2


    Bug-fixes since 0.3.0-beta1

    • Fix incorrect status code #115
    • Computed annotation should not be added for Identity property #116


    Improvements since 0.3.0-beta1

    • Automatically start TripPin service when running E2E cases #146
    • No need to change machine configuration for running tests under Release mode
  • 6.5 Release notes for RESTier 0.4.0-rc

    Features supported in 0.4.0-rc

    • Unified hook handler mechanism for users to inject hooks, Tutorial
    • Built-in RestierController now handles most CRUD scenarios for users including entity set access, singleton access, entity access, property access with $count/$value, $count query option support. #136, #193, #234, Tutorial
    • Support building entity set, singleton and operation from Api (previously Domain). Support navigation property binding. Now users can save much time writing code to build model. #207, Tutorial
    • Support in-memory data source provider #189


    Bug-fixes since 0.3.0-beta2

    • Fix IISExpress instance startup issue in E2E tests #145, #241
    • Should return 400 if there is any invalid query option #176
    • EF7 project bug fixes #253, #254


    Improvements since 0.3.0-beta2

    • Thorough API cleanup, code refactor and concept reduction #164
    • The Conventions project was merged into the Core project. Conventions are now enabled by default. The OnModelExtending convention was removed due to inconsistency. #191
    • Add a sample service with an in-memory provider #189
    • Unified exception-handling process #24, #26
    • Simplified MapRestierRoute now takes an Api class instead of a controller class. No custom controller required in simple cases.
    • Update project URL in RESTier NuGet packages.
  • 6.6 Release notes for RESTier 0.4.0-rc2

    Bug-fixes since 0.4.0-rc

    • Support string as return type or argument of functions #258
  • 6.7 Release notes for RESTier 0.5.0-beta

    New features since 0.4.0-rc2

    • [Issue #150] [PR #286] Integrate Microsoft Dependency Injection Framework into RESTier. Tutorial.
    • [Issue #273] [PR #278] Support temporal types in Restier.EF. Tutorial.
    • [Issue #383] [PR #402] Adopt Web OData Conversion Model builder as default EF provider model builder. Tutorial.
    • [Issue #360] [PR #399] Support $apply in RESTier. Tutorial.

    • Bug-fixes since 0.4.0-rc2

    • [Issue #123] [PR #294] Fix a bug that prevents using Edm.Int64 as entity key.
    • [Issue #269] [PR #271] Fix a bug that NullReferenceException is thrown when POST/PATCH/PUT with null property values.
    • [Issue #287] [PR #314] Fix a bug that $count does not work correctly when there is $expand.
    • [Issue #304] [PR #306] Fix a bug that GetModelAsync is not thread-safe.
    • [Issue #304] [PR #322] Fix a bug that if GetModelAsync takes too long to complete, any subsequent request will fail.
    • [Issue #308] [PR #313] Fix a bug that NullReferenceException is thrown when ColumnTypeAttribute does not have a TypeName property specified.
    • [Issue #309][Issue #310][Issue #311][Issue #312] [PR #313] Fix various bugs in the RESTier query pipeline.


    API changes since 0.4.0-rc2

    • The concept of hook handler now becomes API service after DI integration.
    • The interface IHookHandler and IDelegateHookHandler are removed. The implementation of any custom API service (previously known as hook handler) should also change accordingly. But this should not be big change. Please see Tutorial for details.
    • AddHookHandler is now replaced with AddService from DI. Please see Tutorial for details.
    • GetHookHandler is now replaced with GetApiService and GetService from DI. Please see Tutorial for details.
    • All the serializers and DefaultRestierSerializerProvider are now public. But we still need to address #301 to allow users to override the serializers.
    • The interface IApi is now removed. Use ApiBase instead. We never expect users to directly implement their API classes from IApi anyway. The Context property in IApi now becomes a public property in ApiBase.
    • Previously the ApiData class is very confusing. Now we have given it a more meaningful name DataSourceStubs which accurately describes the usage. Along with this change, we also rename ApiDataReference to DataSourceStubReference accordingly.
    • ApiBase.ApiConfiguration is renamed to ApiBase.Configuration to keep consistent with ApiBase.Context.
    • The static Api class is now separated into two classes ApiBaseExtensions and ApiContextExtensions to eliminate the ambiguity regarding the previous Api class.

7. TOOLING

  • 7.1 Restier Scaffolding

    Introduction

    This tool is used to modify the config class to simplifies the process of building the OData service with EF by Restier(>=0.4.0-rc) in visual studio. The scaffolding item will appear in the scaffolding list by right click on any folder in project and select “Add” -> “New Scaffolded Item”

    Install Visual Studio Extension of Scaffolding

    The installer of Restier scaffolding can be downloaded from Visual Studio Gallery: Restier Scaffolding. Double click vsix to install, the extension supports the VS2013 and VS2015, now.

    Using Scaffolding Tool

    Here is the process of building an OData V4 endpoint using RESTier. With scaffolding tool, you only need to “Create a project and a web app”, then “Generate the model classes”. The project will looks like:

    1. Right click the APP_Start folder->Add->New Scaffolded items
    2. Select “Microsoft OData Restier Config” under Common\Web API node
    3. Select the “Data context class” needed and “WebApi config class” which will be modified to add the code as following:
    4. Click “Change”. Scaffolding tool will add the code in “WebApiConfig.cs”. And add Restier assembly as reference
    5. Reopen the “WebApiConfig.cs” to view the code added:
    6. Rebuld the project and start:

    Notice: The alpha version of tool may contain an issue: during the step 5 and 6, visual studio may need to be restarted.

8. OTHERS

  • 8.1 Sample Services

    Refer to sample service github for end to end sample service.

    The source code also contains end to end service for end to end test purpose.

    All the sample services can be run with visual studio 2015.

9. THANK YOU!

  • 9.1 Vendors

    We’re using NDepend to analyze and increase code quality.

    NDepend