Deep insert support in OData Web API

Kennedy Kangethe Munga

Background

In OData Web API 7.7.0, we added support for deep insert. In deep insert, we create an object and its related items or link existing items in a single request.

This blog post is a continuation of Bulk Operations Support in OData Web API. In that blog post, we explained how to use ODataAPIHandler and ODataAPIHandlerFactory classes. We will not repeat that in this blog post. Bulk update and deep insert share the same ODataAPIHandler and ODataAPIHandlerFactory classes.

In the following sections, we will cover how deep insert is implemented in OData Web API and the things that the developer needs to do to use it in their OData service.

Controller

[ODataRoute("Customers")]
[HttpPost]
[EnableQuery]
public IActionResult Post([FromBody] Customer customer)
{
    var handler = new CustomerAPIHandler();

    handler.DeepInsert(customer, Request.GetModel(), new APIHandlerFactory(Request.GetModel()));

    return Created(customer);
}

For the controller action to handle deep insert, the HttpPost attribute must be applied together with the EnableQuery attribute to ensure that the response has the navigation properties expanded to the level of the request.

According to the OData specOn success, the service MUST create all entities and relate them. If the service responds with 201 Created, the response MUST be expanded to at least the level that was present in the deep-insert request.”  

IExpandQueryBuilder interface

IExpandQueryBuilder is the base interface for generating an $expand query parameter from a payload. Currently we only have one method in the interface.

public interface IExpandQueryBuilder
{
    string GenerateExpandQueryParameter(object value, IEdmModel model);
}

The default implementation of the IExpandQueryBuilder interface is the ExpandQueryBuilder class.

public class ExpandQueryBuilder : IExpandQueryBuilder
{
    /// <inheritdoc />
    public virtual string GenerateExpandQueryParameter (object value, IEdmModel model)
    {
        return expandString;
    }
}

This is the class that makes it possible to expand the navigation properties in the response. The class takes the payload object and generates an expand query parameter which is then appended to the HttpRequest.

The GenerateExpandQueryParameter method takes a payload object and IEdmModel and returns an $expand string.

For example: If we have an Employee object with nested Friends and NewFriends as below:

Employee employee = new Employee
{
    ID = 1,
    Friends = new List<Friend>
    {
        new Friend
        {
            Id = 1001,
            Orders = new List<Order> { new Order { Id = 10001 } }
        }
    },
    NewFriends = new List<NewFriend>
    {
        new NewFriend
        {
            Id = 1001,
            NewOrders = new List<NewOrder> { new NewOrder { Id = 10001 } }
        }
    }
};

We return an expand string: $expand=Friends($expand=Orders),NewFriends($expand=NewOrders)

In the above block of code, Friends property has nested Orders property and NewFriends property has nested NewOrders property.

The customer can provide their own custom implementation of IExpandQueryBuilder and inject it into the dependency injection container as follows:

In the Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddOData();
    services.AddMvc(options =>
    {
        options.EnableEndpointRouting = false;
    });

    services.AddSingleton<IExpandQueryBuilder, CustomExpandQueryBuilder>();
}

The CustomExpandQueryBuilder should implement the IExpandQueryBuilder interface or alternatively subclass the ExpandQueryBuilder class and override the virtual methods.

OData API Handler

In the controller method above, we initialized a CustomerAPIHandler class.

The ODataAPIHandler<TStructuralType> has a default implementation of DeepInsert method which can be used.

public abstract class ODataAPIHandler<TStructuralType>: IODataAPIHandler where TStructuralType : class
{
    // Other  methods
    public virtual void DeepInsert(TStructuralType resource, IEdmModel model, ODataAPIHandlerFactory apiHandlerFactory)
    {
        if (resource == null || model == null)
        {
            return;
        }

        // Other code

        CopyObjectProperties(resource, model, this, apiHandlerFactory);
    }
    
    internal static void CopyObjectProperties(object resource, IEdmModel model, IODataAPIHandler apiHandler, ODataAPIHandlerFactory apiHandlerFactory)
    {
        if (resource == null || model == null || apiHandler == null)
        {
            return;
        }
        // Other code
    }
}

We can override the DeepInsert method and add our custom implementation.

public class CustomerAPIHandler : ODataAPIHandler<Customer>
{
    public override void DeepInsert(TStructuralType resource, IEdmModel model, ODataAPIHandlerFactory apiHandlerFactory)
    {
        base.DeepInsert(resource, model, apiHandlerFactory);
    }
}

Query options

We can add query options in our POST request. If there is an expand query parameter in the query options, we will not generate $expand query parameters using the ExpandQueryBuilder.

POST http://localhost:6285/odata/Customers?$expand=Orders
Content-Type: application/json
{
    "Id": 1,
    "Name": "Customer1",
    "Orders": [
        {
            "@odata.id": "Customers(3)/Orders(1005)"
        },
        {
            "Id": 2000,
            "Price": 200,
            "Quantity": 90
        }
    ]
}

Below will be the response:

{
    "@odata.context": "http://localhost:6285/odata/$metadata#Customers(Orders())/$entity",
    "Id": 1,
    "Name": "Customer1",
    "Age": 0,
    "Orders": [
        {
            "Id": 1005,
            "Price": 40,
            "Quantity": 15
        },
        {
            "Id": 2000,
            "Price": 200,
            "Quantity": 90
        }
    ]
}

 

0 comments

Discussion is closed.

Feedback usabilla icon