RESTier

原文来自: http://odata.github.io/RESTier/

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

    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.

    namespace HelloWorld
    {
        public static class WebApiConfig
        {
            public async static void Register(HttpConfiguration config)
            {
                // enable query options for all properties
                config.Filter().Expand().Select().OrderBy().MaxTop(null).Count();
                await config.MapRestierRoute<EntityFrameworkApi<AdventureWorksLT>>(
                    "AdventureWorksLT",
                    "api/AdventureWorksLT",
                    new RestierBatchHandler(GlobalConfiguration.DefaultServer));
            }
        }
    }

    Note : DbApi was renamed to EntityFrameworkApi from version 0.5.

    The configuration “config.Filter().Expand().Select().OrderBy().MaxTop(null).Count();” is enabling filter/expand/select/orderby/count on all properties, starting 1.0 release, there are more smaller granularity control on the properties which can be used in query option, and all properties are disabled to be used by default. User can add configured in CLR class or during model build to configure which properties are allowed to be used in filter/expand/select/orderby/count. Refer to Model bound document for more details.

    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

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

    One item is special in RESTier, there is only one controller named RESTier controller, and user can implement other additional controllers which extends ODataController class, in order to have consistent authentication for all these controllers, user need to configure Authentication and Authorization filter at global level.

    Restier also provides capability to inspect each request, refer section 2.8 and 2.9 for more details.

    For Web Api security, refer to Web Api Security document.

    Note: Restier uses asynchronous call to the provider layer (entity framework), this means by default the principal used to logic the application will not passed to call to provider, if the application need to pass principal from application to provider layer, refer to this link on the detail configuration.

  • 2.2 Entity Set Filters

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

    1. The filter method name must be OnFilter[entity type name], ending with the target entity type 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 OnFilterProduct method will filter some Product entities by checking the ProductID.

    namespace AdventureWorksLTSample.Models
    {
        public class AdventureWorksApi : EntityFrameworkApi<AdventureWorksContext>
        {
            protected IQueryable<Product> OnFilterProduct(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. 
    

    Note: 1. Starting from version 0.6, the conversion name is changed to OnFilter[entity type name], and before version 0.6, the name is OnFilter[entity set name]

    1. Starting from version 0.6, the filter is applied to all places besides the top level entity set which includes navigation properties, collection of entity in $expand, collection in filter and so on. Refer to end to end test case TrippinE2EOnFilterTestCases for all the scenarios supported.
    2. More meaningful filter can be adopted like filter entity by the owner and the entity owner is current request user.
  • 2.3 Submit Logic

    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.Customize submit logic with single class for all entity set is also 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 ETag Support

    RESTier supports ETag starting from version 0.6.In order to support etag, the entity clr class must have some properties which are marked with [ConcurrencyCheck] attribute, then the properties with this attribute will be used to calculate the etag for concurrency support purpose.

    The etag support is divided into two parts,

    First part is the “@odata.etag” annotation support, it is part of response body, and will be auto added for any entity type which has properties with ConcurrencyCheck attribute when the request is a single entity or a collection of entity (in collection case, each entity instance will have “@odata.etag” annotation).

    Second part is Etag header support, this is only support when operation is against a single entity. Here are the summary of the behavior.

    Operation Header Etag matched? Response
    Get If-Match No 412(Precondition failed)
    Yes Return the entity
    If-None-Match No Return the entity
    Yes 304(Not modified)
    Update/Delete No header 428 (Precondition required)
    If-Match No 412(Precondition failed)
    Yes Proceed the operation
    If-None-Match No Proceed the operation
    Yes 412(Precondition failed)

    In order to support the get method return the etag header, this configuration is a must,

    config.MessageHandlers.Add(new ETagMessageHandler());
    

    User can define his own message handler to set the etag header in the response.

    For both etag annotation and etag header, the algorithm to generate the etag can be replaced, user can create his own etag handler, then set in the config, and default is DefaultODataETagHandler. The code to set customize etag handler is as following,

    IETagHandler eTagHandler = new CustomizedETagHandler();
    config.SetETagHandler(eTagHandler);
    

    For detail end to end examples, refer to end to end test case TrippinE2EEtagTestCases.

    With etag support, the entity can be operated in concurrency mode.

  • 2.5 Model Building

    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.

    namespace Microsoft.OData.Service.Sample.TrippinInMemory
    {
        public class TrippinApi : ApiBase
        {
            protected static new IServiceCollection ConfigureApi(Type apiType, IServiceCollection services)
            {
                services.AddService<IModelBuilder, CustomizedModelBuilder>();
                return ApiBase.ConfigureApi(apiType, 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.

    • Has Resource attribute
    • 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:

    namespace Microsoft.OData.Service.Sample.Trippin.Api
    {
        public class TrippinApi : EntityFrameworkApi<TrippinModel>
        {
            [Resource]
            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.

    • Has Resource attribute
    • 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:

    namespace Microsoft.OData.Service.Sample.Trippin.Api
    {
        public class TrippinApi : EntityFrameworkApi<TrippinModel>
        {
            ...
            [Resource]
            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 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", IsBound = true, 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", IsBound = true, 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. Starting from version 0.6, the operation namespace will be same as the entity type by default if it is not specified, if the namespace is specified in operation attribute, then that namespaces in attribute will be used. Also in order to support operation bound to type like complex type/primitive type besides the entity type, IsBound flag must be set to true for bound operation.
    4. Starting from version 0.6, operation will be auto routed to method defined in Api class, no additional controller is needed. 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 ApiBase.ConfigureApi(apiType, services).

    namespace Microsoft.OData.Service.Sample.Trippin.Api
    {
        public class TrippinApi : ApiBase
        {
            protected static new IServiceCollection ConfigureApi(Type apiType, IServiceCollection services)
            {
                services = ApiBase.ConfigureApi(apiType, 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 ApiBase.ConfigureApi(apiType, services) is called first.
    • RESTier’s model builder includes EF model builder and RestierModelExtender will be called.
    • User’s model builder registered after ApiBase.ConfigureApi(apiType, 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

    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

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

    config.SetUrlKeyDelimiter(ODataUrlKeyDelimiter.Slash);

    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

    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 static new IServiceCollection ConfigureApi(Type apiType, IServiceCollection services)
            {
                // Add OData Query Settings and valiadtion settings
                Func<IServiceProvider, ODataValidationSettings> validationSettingFactory = (sp) => new ODataValidationSettings
                {
                    MaxAnyAllExpressionDepth =3,
                    MaxExpansionDepth = 3
                };
    
                return ApiBase.ConfigureApi(apiType, 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 static new IServiceCollection ConfigureApi(Type apiType, IServiceCollection services)
            {
    
                return ApiBase.ConfigureApi(apiType, 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

    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 static new IServiceCollection ConfigureApi(Type apiType, IServiceCollection services)
            {
                return EntityFrameworkApi<TrippinModel>.ConfigureApi(apiType, 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

    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 static new IServiceCollection ConfigureApi(Type apiType, IServiceCollection services)
            {
                return ApiBase.ConfigureApi(apiType, 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

    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 static new IServiceCollection ConfigureApi(Type apiType, IServiceCollection services)
            {
                return ApiBase.ConfigureApi(apiType, services)
                    .AddSingleton<ODataSerializerProvider, CustomizedSerializerProvider>();
            }

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

  • 2.12 Derived Type Support

    RESTier supports derived type starting from version 0.6.Derived type support does not need any additional configuration, it is supported by default.

    Refer to method CURDDerivedEntity in class TrippinE2ETestCases and method DerivedTypeQuery in class TrippinE2EQueryTestCases to see the end to end example.

    Note: In a typical IIS configuration, the dot in this URL will cause IIS to return error 404. You can resolve this by adding the following section to your Web.Config file:

    <system.webServer>
        <handlers>
          <clear/>
          <add name="ExtensionlessUrlHandler-Integrated-4.0" path="/*" 
              verb="*" type="System.Web.Handlers.TransferRequestHandler" 
              preCondition="integratedMode,runtimeVersionv4.0" />
        </handlers>
    </system.webServer>  
    
  • 2.13 Spatial Type Support

    RESTier supports spatial type starting from version 0.6 with some manual effort.There are two ways to support spatial type, refer to the end to end samples for more detail.

    SpatialSample will need few manual effort to build model, but there is one limitation that user can not have the spatial type property in query option.

    SpatialSample2 will require to use Edm model builder to build model, have full support of spatial type.

3. EXTENSIONS

  • 3.1 Use temporal types in RESTier Entity Framework

    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.

    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.

    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.

    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.

    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

    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

    Operation includes function (bounded), function import (unbounded), action (bounded), and action(unbounded).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 controller action.

    Starting from release 0.6.0, RESTier can auto route an operation request to the method defined in API class which is defined for operation model building, user does NOT need to define its own controller with ODataRoute attribute for operation route.

    Refer to end to end test cases for end to end operation support samples.

    Note: In a typical IIS configuration, the dot in this URL will cause IIS to return error 404. You can resolve this by adding the following section to your Web.Config file:

    <system.webServer>
        <handlers>
          <clear/>
          <add name="ExtensionlessUrlHandler-Integrated-4.0" path="/*" 
              verb="*" type="System.Web.Handlers.TransferRequestHandler" 
              preCondition="integratedMode,runtimeVersionv4.0" />
        </handlers>
    </system.webServer>  
    
  • 3.4 In-Memory Provider

    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:

    namespace Microsoft.OData.Service.Sample.TrippinInMemory
    {
        public class TrippinApi : ApiBase
        {
            private static readonly List<Person> people = new List<Person>
            {
                ...
            };
    
            [Resource]
            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 static new IServiceCollection ConfigureApi(Type apiType, IServiceCollection services)
            {
                services.AddService<IModelBuilder>(new ModelBuilder());
                return ApiBase.ConfigureApi(apiType, 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.

    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 API Service

    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 static new IServiceCollection ConfigureApi(Type apiBase, IServiceCollection services)
        {        
                // 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
                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 static new IServiceCollection ConfigureApi(Type apiType, IServiceCollection services)
        {
            return ApiBase.ConfigureApi(apiType, 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 static new IServiceCollection ConfigureApi(Type apiType, 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 securityYou 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 viewsCustomized 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 filtersWith 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 logicWith 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.
  • 6.8 Release notes for RESTier 0.6.0

    New features since 0.5.0-beta

    Bug-fixes since 0.5.0-beta

    • [Issue #432] [PR #452] More meaningful exception message during model build.
    • [Issue #438] [PR #448] Make namespace consistent for entity type / operation / container in model.
    • [Issue #413] [PR #454] Auto pop entity type key from entity framework during model builder.
    • [Issue #426] [PR #424] Support method return nullable enum.
    • [Issue #459] [PR #458] Return 404 if single entity for bound operation does not exist.
    • [Issue #288] [PR #465] Return 204 if single navigation property is null.
    • [Issue #328] [PR #465] Return 404 when request property of non-exist entity or complex.
    • [PR #455] Improve RESTier routing which only fail to entity set controller if there is an action for the request.
  • 6.9 Release notes for RESTier 1.0.0

    This is the first GA release of RESTier, it is based on newest odata .net library 7.x and newest Web API OData library 6.x.Now it is more flexibility for user to define the behavior, and lots more customization capability as both ODL and WAO have adopted Dependency Injection. All the service registered in DI container by ODL and WAO can be customized. Refer to ODL and WAO documents for more detail.

    Three breaking changes impacts any consumer who implements its own API are,

    1. Must create a constructor which accept a IServiceProvider, the IServiceProvider will be set automatically when Api instance is retrieved from DI container.
    2. ConfigureApi method is static and has one additional parameter “Type apiType”, and in this method, it must call ApiBase or EntityFrameworkApi ConfigureApi method to have Api registered as DI service.
    3. Starting 1.0 release, there are more smaller granularity control on the properties which can be used in query option, and all properties are disabled to be used by default. User can add configured in CLR class or during model build to configure which properties are allowed to be used in filter/expand/select/orderby/count. Refer to Model bound document for more details. User can also use configuration “config.Filter().Expand().Select().OrderBy().MaxTop(null).Count();” to enable filter/expand/select/orderby/count on all properties.

    New features since 0.6.0

    • Add resource attribute for the properties in Api class
      • Now for the properties in API class, it must have Resource attribute to be built as entity set or singleton.
    • Make URI resolver as DI service.
      • URL resolver will need to be registered as DI service which was set via config before.
    • Move ApiBase as DI service and remove ApiContext.
      • ApiBase is DI service now and one instance is created for each request. Remove ApiContext to make logic clear.
    • Make ApiConfiguration as internal service.
      • ApiConfiguration is moved as internal service now, if additional static configuration is needed for Api class, a DI singleton service is recommeded.
    • Use WepApi OData formatting attribute and remove RESTier formatting attribute.
    • Add support of untyped property

    Bug-fixes since 0.6.0

    • [Issue #491] [PR #495] Support property with type of byte array as concurrency check properties.
    • [Issue #488] [PR #498] Support DataTime whose kind is local.
    • [Issue #505] [PR #507] PATCH semantics against complex types is incorrect.

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

  • 8.2 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. UnCheck Require source files to exactly match the original version.
    4. Check Enable source server support.

    Setup your symbol source in Symbols tab:

    1. Check Microsoft Symbol Servers.
    2. Add location: http://srv.symbolsource.org/pdb/Public (For preview/public releases in nuget.org).
    3. Add location: http://srv.symbolsource.org/pdb/MyGet (For nightly build, and preview releases in myget.org).
    4. 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.

    RESTier also exposes a configuration which will return the whole exception stack trace if there is any exception thrown on the server side for the request, it is disabled by default, it can be enabled via call “config.SetUseVerboseErrors(true);” during routes register.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.