expressappframework-405468-backend-web-api-service-use-odata-to-send-requests-deep-update-and-batch-http-requests-to-web-api.md
The XAF Backend Web API Service enables deep insert and update operations using POST, PATCH, and PUT methods. You can create or modify business objects with their referenced objects in a single request. Batch operations allow you to execute multiple changes across different object types in one HTTP request.
Example code in this help topic applies to a project that uses EF Core. If you use XPO for data access, replace ID property references with Oid.
Tip
Code samples in this topic are extracted from the MainDemo.WebAPI.Tests project installed as part of the XAF package. The default location of the project: %PUBLIC%\Documents\DevExpress Demos 25.2\Components\XAF\MainDemo.NET.EFCore\CS\MainDemo.WebAPI.Tests.
To obtain the JWT Authentication token for data requests, send a request to api/Authentication/Authenticate and specify a username and password. The following example uses Sam as the username and an empty password.
Note that for patch and post requests, the server returns only the HTTP status code. Add a Prefer header to a request and set its value to return=representation to include the modified content in the response body.
HttpClient httpClient = new() { BaseAddress = new Uri("https://localhost:44319") };
StringContent httpContent = new(@"{ ""userName"": ""Sam"", ""password"": """" }", Encoding.UTF8, "application/json");
var tokenResponse = await httpClient.PostAsync("/api/Authentication/Authenticate", httpContent);
var token = await tokenResponse.Content.ReadAsStringAsync();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
httpClient.DefaultRequestHeaders.Add("OData-Version", "4.01");
httpClient.DefaultRequestHeaders.Add("Prefer", "return=representation");
Note
For more information about cookie or JWT authentication in JavaScript instead of .NET, review the following article: JavaScript — Consume the DevExpress Backend Web API with Svelte (Part 5. Authenticate Users and Protect Data). See the following code file in the associated GitHub Example: src/hooks.server.js.
The following code creates a new Employee object with an associated Task object in a single request:
StringContent content = new(@"{
""FirstName"": ""Mary"",
""LastName"":""Gordon"",
""Email"":""[email protected]"",
""Tasks"": [
{ ""Description"":""Foo"",""Subject"":""Bar"" }
]
}", Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync("/api/odata/Employee?$expand=Tasks", content);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
Display example result
{
"@context": "http://localhost/api/odata/$metadata#Employee(Tasks())/$entity",
"ID": "b40bcbad-0c66-4688-60c5-08dd895281b7",
"FullName": "Mary Gordon",
"FirstName": "Mary",
"LastName": "Gordon",
"Email": "[email protected]",
"Tasks": [
{
"ID": "5ca52dbf-c812-4e24-b057-08dd895281c3",
"Subject": "Bar",
"Description": "Foo",
"PercentCompleted": 0,
"Status": "NotStarted",
"Priority": "Low",
"ActualWorkHours": 0,
"EstimatedWorkHours": 0
}
]
}
The code sample in this section performs the following operations in one request:
Employee object with the specified ID and creates a referenced Department object.Employee already has an associated Department object, the code updates the existing object. To learn how to create a new object and replace the reference property value, refer to the following section: Replace a Referenced Object.Position objects for the Department object.var employee_ID = "b40bcbad-0c66-4688-60c5-08dd895281b7";
// ...
using var createDepartmentResponse = await httpClient.PatchAsync(
$"/api/odata/Employee/{employee_ID}?$expand=Department($expand=Positions)",
new StringContent(
$@"{{
""Department"": {{
""Title"": ""Logistics"",
""DepartmentHead"": {{ ""ID"": ""{employee_ID}"" }},
""Positions"": [
{{ ""Title"": ""Logistics Head"" }},
{{ ""Title"": ""Logistics Head Assistant"" }}
]
}}
}}", Encoding.UTF8, "application/json"));
createDepartmentResponse.EnsureSuccessStatusCode();
var responseContent = await createDepartmentResponse.Content.ReadAsStringAsync();
Display example result
{
"@context": "http://localhost/api/odata/$metadata#Employee(Department(Positions()))/$entity",
"ID": "b40bcbad-0c66-4688-60c5-08dd895281b7",
"FullName": "Mary Gordon",
"FirstName": "Mary",
"LastName": "Gordon",
"Email": "[email protected]",
"Department": {
"ID": "2f4c580f-1d17-4be7-bda8-08dd8662f8d7",
"Title": "Logistics",
"Positions":[
{
"ID":"5561593f-5f55-4f92-9f74-08dd8cb0f592",
"Title":"Logistics Head"
},
{
"ID":"6afc0a3e-392f-483b-9f75-08dd8cb0f592",
"Title":"Logistics Head Assistant"
}
]
}
}
The following code sample updates the title of the Department object associated with the Employee object:
using var updateDepartmentResponse = await httpClient.PatchAsync(
$"/api/odata/Employee/b40bcbad-0c66-4688-60c5-08dd895281b7?$expand=Department",
new StringContent(
@"{
""Department"": {
""Title"": ""Logistics And Warehouse""
}
}", Encoding.UTF8, "application/json"));
updateDepartmentResponse.EnsureSuccessStatusCode();
var responseContent = await updateDepartmentResponse.Content.ReadAsStringAsync();
Display example result
{
"@context": "http://localhost/api/odata/$metadata#Employee(Department())/$entity",
"ID": "b40bcbad-0c66-4688-60c5-08dd895281b7",
"FullName": "Mary Gordon",
"FirstName": "Mary",
"LastName": "Gordon",
"Email": "[email protected]",
"Department": {
"ID": "2f4c580f-1d17-4be7-bda8-08dd8662f8d7",
"Title": "Logistics And Warehouse"
}
}
To replace a referenced object, specify the new object ID. The original object remains intact.
ID property value to an existing object’s ID to reference this object.ID property to its default value to create a new referenced object.0, and for Guid, it is represented by the Guid.Empty field (00000000-0000-0000-0000-000000000000).Other specified property values are applied to the newly created or assigned referenced object.
The following code sample finds an Employee object by ID and creates a new referenced Department object:
using var newDepartmentLinkResponse = await httpClient.PatchAsync(
$"/api/odata/Employee/4011fcb7-d8bf-4c95-2e7a-08dd8661082a?$expand=Department($expand=Positions)",
new StringContent(
$@"{{
""Department"": {{
""ID"": ""{System.Guid.Empty}"",
""Title"": ""Sales"",
""DepartmentHead"": {{ ""ID"": ""4011fcb7-d8bf-4c95-2e7a-08dd8661082a"" }},
""Positions"": [{{ ""Title"": ""Sales Head Assistant"" }}]
}}
}}", Encoding.UTF8, "application/json"));
newDepartmentLinkResponse.EnsureSuccessStatusCode();
var responseContent = await newDepartmentLinkResponse.Content.ReadAsStringAsync();
Display example result
{
"@context": "http://localhost/api/odata/$metadata#Employee(Department(Positions()))/$entity",
"ID": "4011fcb7-d8bf-4c95-2e7a-08dd8661082a",
"FullName": "Mary Gordon",
"FirstName": "Mary",
"LastName": "Gordon",
"Email": "[email protected]",
"Department": {
"ID": "4349F735-19B8-4DFE-8C18-88117B855EDD",
"Title": "Sales",
"Positions":[
{
"ID":"5561593f-5f55-4f92-9f74-08dd8cb0f592",
"Title":"Sales Head Assistant"
}
]
}
}
Use the @delta annotation to send a list of modifications in a request. The code sample in this section performs the following actions:
New test task 100…5450) to the current employee…f585) EstimatedWorkHours value…b018) from the current employee…d4fa)using var updateTasksResponse = await httpClient.PatchAsync(
"/api/odata/Employee",
new StringContent(
$@"{{
""@context"": ""http://localhost/api/odata/$metadata#Employee/$delta"",
""value"": [
{{
""ID"": ""{employeeKey}"",
""Tasks@delta"": [
{{
""Subject"": ""New test task 100""
}},
{{
""ID"": ""3f5e43dd-7b1a-45b1-ce31-08dd7db05450""
}},
{{
""ID"": ""e847e4a3-d151-41e2-5a5f-08dd8723f585"",
""EstimatedWorkHours"": 15
}},
{{
""@removed"": {{ ""reason"": ""changed"" }},
""ID"": ""70461055-5c93-48c8-6dfd-08dd87f5b018""
}},
{{
""@removed"": {{ ""reason"": ""deleted"" }},
""ID"": ""a74366e4-b391-4e01-3234-08dd87f7d4fa""
}}
]
}}
]
}}", Encoding.UTF8, "application/json"));
updateTasksResponse.EnsureSuccessStatusCode();
responseContent = await updateTasksResponse.Content.ReadAsStringAsync();
Display example result
{"@context": "http:localhost/api/odata/$metadata#Employee/$delta",
"value": [
{
"ID": "4011fcb7-d8bf-4c95-2e7a-08dd8661082a",
"Tasks@delta": [
{
"ID": "13632d9d-9875-411a-dd93-08dd87f9079c",
"Subject": "New test task 100"
},
{
"ID": "3f5e43dd-7b1a-45b1-ce31-08dd7db05450"
},
{
"ID": "e847e4a3-d151-41e2-5a5f-08dd8723f585",
"EstimatedWorkHours": 15
},
{
"@removed": {
"reason": "changed"
},
"@id": "http:localhost/api/odata/DemoTask(70461055-5c93-48c8-6dfd-08dd87f5b018)",
"ID": "70461055-5c93-48c8-6dfd-08dd87f5b018"
},
{
"@removed": {
"reason": "deleted"
},
"@id": "http:localhost/api/odata/DemoTask(a74366e4-b391-4e01-3234-08dd87f7d4fa)",
"ID": "a74366e4-b391-4e01-3234-08dd87f7d4fa"
}
]
}
]
}
You can create, update, and delete several objects of the same type in one request. The following code sample creates two DemoTask objects:
using var createTaskResponse = await httpClient.PatchAsync("/api/odata/DemoTask",
new StringContent(
$@"{{
""@context"": ""http://localhost/api/odata/$metadata#DemoTask/$delta"",
""value"": [
{{ ""Subject"": ""Test task 1"" }},
{{ ""Subject"": ""Test task 2"" }}
]
}}", Encoding.UTF8, "application/json"));
createTaskResponse.EnsureSuccessStatusCode();
var responseContent = await createTaskResponse.Content.ReadAsStringAsync();
Display example result
{
"@context":"http://localhost/api/odata/$metadata#DemoTask/$delta",
"value":[
{
"ID":"a82866a5-9793-4e4c-a17c-23eb0736c5e7",
"Subject":"Test task 1"
},
{
"ID":"59b76c2b-0539-48db-b7e5-49563db7a5e5",
"Subject":"Test task 2"
}
]
}
Batch query allows you to request several objects in a single HTTP request to the service. The following code sample sends two queries in a single HTTP request:
Employee object by its IDDepartment object by its ID along with its referenced Position objectsvar batchRequestContent = @$"{{
""requests"": [
{{
""method"": ""{HttpMethod.Get}"",
""url"": ""{$"/api/odata/{typeof(Employee).Name}/e1574d97-fafd-46be-21cd-08dd8d554255"}"",
""headers"": {{
""content-type"": ""application/json; odata.metadata=minimal; odata.streaming=true"",
""odata-version"": ""4.01""
}},
""id"": ""0""
}},
{{
""method"": ""{HttpMethod.Get}"",
""url"": ""{$"/api/odata/{typeof(Department).Name}/c666e9e6-bd06-4b17-04a5-08dd8d554376"}?$expand=Positions"",
""headers"": {{
""content-type"": ""application/json; odata.metadata=minimal; odata.streaming=true"",
""odata-version"": ""4.01""
}},
""id"": ""1""
}}
]
}}";
var httpResponse = await httpClient.PostAsync("/api/odata/$batch", new StringContent(batchRequestContent, Encoding.UTF8, "application/json"));
httpResponse.EnsureSuccessStatusCode();
string responseContent = await httpResponse.Content.ReadAsStringAsync();
Display example result
{"responses":[
{
"id":"0",
"status":200,
"headers":{"content-type":"application/json; odata.metadata=minimal; odata.streaming=true; charset=utf-8","odata-version":"4.01"},
"body" :{
"@context":"http://localhost/api/odata/$metadata#Employee/$entity",
"ID":"e1574d97-fafd-46be-21cd-08dd8d554255",
"FullName":"Stefan Johnson",
"FirstName":"Stefan",
"LastName":"Johnson",
"Email":"[email protected]",
}
},
{
"id":"1",
"status":200,
"headers":{"content-type":"application/json; odata.metadata=minimal; odata.streaming=true; charset=utf-8","odata-version":"4.01"},
"body" :{
"@context":"http://localhost/api/odata/$metadata#Department(Positions())/$entity",
"ID":"c666e9e6-bd06-4b17-04a5-08dd8d554376",
"Title":"New department",
"Positions":[
{
"ID":"2462f989-969d-415e-e9ab-08dd8d55437e",
"Title":"New position in new department"
}
]
}
}
]}
Note
Batch modifications allow you to create, update, or delete several objects in a single HTTP request to the service. The following code sample modifies two objects in a single batch request:
Employee object: sets the new value for the FirstName property and changes the title of the linked Department object.Department object: changes the Office property value and requests a Position collection.var batchRequestContent = @$"{{
""requests"": [
{{
""method"": ""{HttpMethod.Patch}"",
""url"": ""{$"/api/odata/{typeof(Employee).Name}/{employee.ID}"}"",
""headers"": {{
""content-type"": ""application/json; odata.metadata=minimal; odata.streaming=true"",
""odata-version"": ""4.01"",
""Prefer"": ""return=representation""
}},
""id"": ""0"",
""body"": {{
""FirstName"": ""Stefan"",
""Department@delta"": {{
""Title"": ""Department new Title""
}}
}}
}},
{{
""method"": ""{HttpMethod.Patch}"",
""url"": ""{$"/api/odata/{typeof(Department).Name}/{department.ID}"}?$expand=Positions"",
""headers"": {{
""content-type"": ""application/json; odata.metadata=minimal; odata.streaming=true"",
""odata-version"": ""4.01"",
""Prefer"": ""return=representation""
}},
""id"": ""1"",
""body"": {{
""Office"": ""New office on the top floor""
}}
}}
]
}}";
var httpResponse = await httpClient.PostAsync("/api/odata/$batch", new StringContent(batchRequestContent, Encoding.UTF8, "application/json"));
httpResponse.EnsureSuccessStatusCode();
string responseContent = await httpResponse.Content.ReadAsStringAsync();
Display example result
{"responses":[
{
"id":"0",
"status":200,
"headers":{"content-type":"application/json; odata.metadata=minimal; odata.streaming=true; charset=utf-8","odata-version":"4.01"},
"body" :{
"@context":"http://localhost/api/odata/$metadata#Employee/$entity",
"ID":"4b304322-5e9c-4b59-5196-08dd8cd06915",
"FullName":"Stefan Johnson",
"FirstName":"Stefan",
"LastName":"Johnson",
"Email":"[email protected]",
}
},
{
"id":"1",
"status":200,
"headers":{"content-type":"application/json; odata.metadata=minimal; odata.streaming=true; charset=utf-8","odata-version":"4.01"},
"body" :{
"@context":"http://localhost/api/odata/$metadata#Department(Positions())/$entity",
"ID":"a3c6d14a-95ad-4bd9-2786-08dd8cd06a53",
"Title":"Department new Title",
"Office":"New office on the top floor",
"Positions":[
{
"ID":"4b304322-5e9c-4b59-5196-08dd8cd06915",
"Title":"New position in new department"
}
]
}
}
]}
Note
Prefer header separately for every request (as demonstrated in this example) or globally as described in the following section: Authenticate with JSON Web Tokens (JWT).