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 spec “On 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