Property Routing in ASP.NET Core OData 8
Applies To:# OData Web API 8 supported OData Web API v8
This tutorial shows how ASP.NET Core OData 8 supports property routing. An understanding of routing fundamentals in ASP.NET Core OData 8 is assumed. If you're unfamiliar with routing in ASP.NET Core OData 8, you may want to go through the routing overview tutorial.
Introduction
OData property routing convention supports the following route templates:
Request Method | Route Template |
---|---|
GET | PUT |
~/{entityset}/{key}/{property} |
GET | PUT |
~/{entityset}/{key}/{cast}/{property} |
GET | PUT |
~/{entityset}/{key}/{property}/{cast} |
GET | PUT |
~/{entityset}/{key}/{cast}/{property}/{cast} |
GET | PUT |
~/{singleton}/{property} |
GET | PUT |
~/{singleton}/{cast}/{property} |
GET | PUT |
~/{singleton}/{property}/{cast} |
GET | PUT |
~/{singleton}/{cast}/{property}/{cast} |
GET |
~/{entityset}/{key}/{primitiveproperty}/$value |
GET |
~/{entityset}/{key}/{cast}/{primitiveproperty}/$value |
GET |
~/{entityset}/{key}/{collectionvaluedproperty}/$count |
GET |
~/{entityset}/{key}/{cast}/{collectionvaluedproperty}/$count |
GET |
~/{singleton}/{primitiveproperty}/$value |
GET |
~/{singleton}/{cast}/{primitiveproperty}/$value |
GET |
~/{singleton}/{collectionvaluedproperty}/$count |
GET |
~/{singleton}/{cast}/{collectionvaluedproperty}/$count |
POST |
~/{entityset}/{key}/{collectionvaluedproperty} |
POST |
~/{entityset}/{key}/{cast}/{collectionvaluedproperty} |
POST |
~/{singleton}/{collectionvaluedproperty} |
POST |
~/{singleton}/{cast}/{collectionvaluedproperty} |
PATCH |
~/{entityset}/{key}/{singlevaluedproperty} |
PATCH |
~/{entityset}/{key}/{cast}/{singlevaluedproperty} |
PATCH |
~/{entityset}/{key}/{singlevaluedproperty}/{cast} |
PATCH |
~/{entityset}/{key}/{cast}/{singlevaluedproperty}/{cast} |
PATCH |
~/{singleton}/{singlevaluedproperty} |
PATCH |
~/{singleton}/{cast}/{singlevaluedproperty} |
PATCH |
~/{singleton}/{singlevaluedproperty}/{cast} |
PATCH |
~/{singleton}/{cast}/{singlevaluedproperty}/{cast} |
DELETE |
~/{entityset}/{key}/{nullableproperty} |
DELETE |
~/{entityset}/{key}/{cast}/{nullableproperty} |
DELETE |
~/{singleton}/{nullableproperty} |
DELETE |
~/{singleton}/{cast}/{nullableproperty} |
Notes:
- OData routing supports canonical parentheses-style key (e.g.
~/Customers(1)
) in addition to key-as-segment (e.g.~/Customers/1
). Currently, ASP.NET Core OData 8 does not support key-as-segment convention in multi-part keys scenarios {cast}
is a placeholder for the fully-qualified name for a derived type
To illustrate property routing convention, let's build a sample OData service.
Prerequisites
Visual Studio 2022 with the ASP.NET and web development workload
Packages
Install the Microsoft.AspNetCore.OData 8.x Nuget package:
In the Visual Studio Package Manager Console:
Install-Package Microsoft.AspNetCore.OData
Models
The following are the models for the OData service:
Customer
class
namespace PropertyRouting.Models
{
using System.Collections.Generic;
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public Address BillingAddress { get; set; }
public List<string> ContactPhones { get; set; } = new List<string>();
}
}
EnterpriseCustomer
class
namespace PropertyRouting.Models
{
using System.Collections.Generic;
public class EnterpriseCustomer : Customer
{
public decimal CreditLimit { get; set; }
public Address RegisteredAddress { get; set; }
public List<Address> ShippingAddresses { get; set; } = new List<Address>();
}
}
Address
class
namespace PropertyRouting.Models
{
public class Address
{
public string City { get; set; }
}
}
PostalAddress
class
namespace PropertyRouting.Models
{
public class PostalAddress : Address
{
public string PostalCode { get; set; }
}
}
Edm model and service configuration
The logic for building the Edm model and configuring the OData service is as follows:
// Program.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OData;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OData.ModelBuilder;
using ActionRouting.Models;
var builder = WebApplication.CreateBuilder(args);
var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Customer>("Customers");
services.AddControllers().AddOData(
options => options.EnableQueryFeatures(null).AddRouteComponents(
routePrefix: "odata",
model: modelBuilder.GetEdmModel()));
var app = builder.Build();
app.UseODataRouteDebug();
app.UseRouting();
app.UseEndpoints(endpoints => endpoints.MapControllers());
app.Run();
In the above block of code, we define an entity set named Customers
. Implicitly, Customer
and EnterpriseCustomer
get included in the Edm model as entity types. Address
and PostalAddress
also get included as complex types because they do not have key properties.
Controller
The partial structure of the controller for the OData service is as follows:
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using PropertyRouting.Models;
public class CustomersController : ODataController
{
private static List<Customer> customers = new List<Customer>
{
new Customer
{
Id = 1,
Name = "Customer 1",
ContactPhones = new List<string> { "761-116-1865" },
BillingAddress = new Address { Street = "Street 1A" }
},
new Customer
{
Id = 2,
Name = "Customer 2",
ContactPhones = new List<string> { "835-791-8257" },
BillingAddress = new PostalAddress { Street = "2A", PostalCode = "14030" }
},
new EnterpriseCustomer
{
Id = 3,
Name = "Customer 3",
ContactPhones = new List<string> { "157-575-6005" },
BillingAddress = new Address { Street = "Street 3A" },
CreditLimit = 4200,
RegisteredAddress = new Address { Street = "Street 3B" },
ShippingAddresses = new List<Address>
{
new Address { Street = "Street 3C" }
}
},
new EnterpriseCustomer
{
Id = 4,
Name = "Customer 4",
ContactPhones = new List<string> { "724-096-6719" },
BillingAddress = new Address { Street = "Street 4A" },
CreditLimit = 3700,
RegisteredAddress = new PostalAddress { Street = "Street 4B", PostalCode = "22109" },
ShippingAddresses = new List<Address>
{
new Address { Street = "Street 4C" }
}
}
};
}
Routing conventions for properties
In this section we cover the conventions for property routing and the controller actions (endpoints) required for the request to be routed successfully.
Retrieving a property
The route templates for this request are:
GET ~/{entityset}({key})/{property}
GET ~/{entityset}/{key}/{property}
GET ~/{singleton}/{property}
The following request returns BillingAddress
property on customer 1:
GET http://localhost:5000/odata/Customers(1)/BillingAddress
For the above request to be conventionally-routed, a controller action named GetBillingAddress
that accepts the key parameter is expected:
public ActionResult<Address> GetBillingAddress([FromRoute] int key)
{
var customer = customers.SingleOrDefault(d => d.Id.Equals(key));
if (customer == null)
{
return NotFound();
}
return customer.BillingAddress;
}
The following JSON payload shows the expected response:
{
"@odata.context": "http://localhost:5000/odata/$metadata#Customers(1)/BillingAddress",
"Street": "Street 1A"
}
Retrieving a property set to an instance of a derived type
The route templates for this request are:
GET ~/{entityset}({key})/{property}/{cast}
GET ~/{entityset}/{key}/{property}/{cast}
GET ~/{singleton}/{property}/{cast}
The following request returns BillingAddress
property on customer 2. The property value is an instance of PostalAddress
- derived from Address
:
GET http://localhost:5000/odata/Customers(2)/BillingAddress/PropertyRouting.Models.PostalAddress
The cast segment (e.g. /PropertyRouting.Models.PostalAddress
) serves the purpose of explicitly specifying the type of instance expected on the property. If the instance is not of the expected type a 404 Not Found
response should be returned.
For the above request to be conventionally-routed, a controller action named GetBillingAddressOfPostalAddress
that accepts the key parameter is expected:
public ActionResult<PostalAddress> GetBillingAddressOfPostalAddress([FromRoute] int key)
{
var customer = customers.SingleOrDefault(d => d.Id.Equals(key));
if (!(customer?.BillingAddress is PostalAddress billingAddress))
{
return NotFound();
}
return billingAddress;
}
The following JSON payload shows the expected response:
{
"@odata.context": "http://localhost:5000/odata/$metadata#Customers(2)/BillingAddress/PropertyRouting.Models.PostalAddress",
"Street": "2A",
"PostalCode": "14030"
}
Note that the controller action from retrieving a property section can adequately handle request for BillingAddress
property that is set to an instance of PostalAddress
derived type. The request would look as follows:
GET http://localhost:5000/odata/Customers(2)/BillingAddress
The following JSON payload shows the expected response. Note the difference between the value of @odata.context
property in the payload below versus the one in preceding payload. In addition, there's an extra @odata.type
annotation property that serves to specify that the payload is a PostalAddress
:
{
"@odata.context": "http://localhost:5000/odata/$metadata#Customers(2)/BillingAddress",
"@odata.type": "#PropertyRouting.Models.PostalAddress",
"Street": "2A",
"PostalCode": "14030"
}
Retrieving a property declared on a derived type
The route templates for this request are:
GET ~/{entityset}({key})/{cast}/{property}
GET ~/{entityset}/{key}/{cast}/{property}
GET ~/{singleton}/{cast}/{property}
The following request returns RegisteredAddress
property on enterprise customer 3. The RegisteredAddress
property is declared on the EnterpriseCustomer
derived type:
GET http://localhost:5000/odata/Customers(3)/PropertyRouting.Models.EnterpriseCustomer/RegisteredAddress
For the above request to be conventionally-routed, a controller action named GetRegisteredAddressFromEnterpriseCustomer
that accepts the key parameter is expected:
public ActionResult<Address> GetRegisteredAddressFromEnterpriseCustomer([FromRoute] int key)
{
var enterpriseCustomer = customers.OfType<EnterpriseCustomer>().SingleOrDefault(d => d.Id.Equals(key));
if (enterpriseCustomer == null)
{
return NotFound();
}
return enterpriseCustomer.RegisteredAddress;
}
The following JSON payload shows the expected response:
{
"@odata.context": "http://localhost:5000/odata/$metadata#Customers(3)/PropertyRouting.Models.EnterpriseCustomer/RegisteredAddress",
"Street": "Street 3B"
}
Retrieving a property declared on a derived type and set to an instance of a derived type
The route templates for this request are:
GET ~/{entityset}({key})/{cast}/{property}/{cast}
GET ~/{entityset}/{key}/{cast}/{property}/{cast}
GET ~/{singleton}/{cast}/{property}/{cast}
The following request returns RegisteredAddress
property on enterprise customer 4. The property value is an instance of PostalAddress
derived type:
GET http://localhost:5000/odata/Customers(4)/PropertyRouting.Models.EnterpriseCustomer/RegisteredAddress/PropertyRouting.Models.PostalAddress
For the above request to be conventionally-routed, a controller action named GetRegisteredAddressOfPostalAddressFromEnterpriseCustomer
that accepts the key parameter is expected:
public ActionResult<PostalAddress> GetRegisteredAddressOfPostalAddressFromEnterpriseCustomer([FromRoute] int key)
{
var enterpriseCustomer = customers.OfType<EnterpriseCustomer>().SingleOrDefault(d => d.Id.Equals(key));
if (!(enterpriseCustomer?.RegisteredAddress is PostalAddress registeredAddress))
{
return NotFound();
}
return registeredAddress;
}
The following JSON payload shows the expected response:
{
"@odata.context": "http://localhost:5000/odata/$metadata#Customers(4)/PropertyRouting.Models.EnterpriseCustomer/RegisteredAddress/PropertyRouting.Models.PostalAddress",
"Street": "Street 4B",
"PostalCode": "22109"
}
Note again that the controller action from retrieving a property declared on a derived type section can adequately handle request for RegisteredAddress
property that is set to an instance of PostalAddress
derived type. The request would look as follows:
GET http://localhost:5000/odata/Customers(4)/PropertyRouting.Models.EnterpriseCustomer/RegisteredAddress
The following JSON payload shows the expected response. Take note of the value of @odata.context
property in addition to the extra @odata.type
annotation property:
{
"@odata.context": "http://localhost:5000/odata/$metadata#Customers(4)/PropertyRouting.Models.EnterpriseCustomer/RegisteredAddress",
"@odata.type": "#PropertyRouting.Models.PostalAddress",
"Street": "Street 4B",
"PostalCode": "22109"
}
Retrieving the raw value of a primitive property
The route templates for this request are:
GET ~/{entityset}({key})/{primitiveproperty}/$value
GET ~/{entityset}/{key}/{primitiveproperty}/$value
GET ~/{singleton}/{primitiveproperty}/$value
The result of retrieving a property is a JSON object. For instance, the following request returns the CreditLimit
primitive property on enterprise customer 3. Note that the CreditLimit
property is declared on the EnterpriseCustomer
derived type:
GET http://localhost:5000/odata/Customers(3)/PropertyRouting.Models.EnterpriseCustomer/CreditLimit
The response is as follows:
{
"@odata.context": "http://localhost:5000/odata/$metadata#Customers(3)/PropertyRouting.Models.EnterpriseCustomer/CreditLimit",
"value": 4200
}
To retrieve just the raw value of the primitive property, append /$value
to that property's URL.
The following request returns the raw value of the CreditLimit
primitive property on enterprise customer 3:
GET http://localhost:5000/odata/Customers(3)/PropertyRouting.Models.EnterpriseCustomer/CreditLimit/$value
For the above request to be conventionally-routed, a controller action named GetCreditLimitFromEnterpriseCustomer
that accepts the key parameter is expected:
public ActionResult<decimal> GetCreditLimitFromEnterpriseCustomer([FromRoute] int key)
{
var enterpriseCustomer = customers.OfType<EnterpriseCustomer>().SingleOrDefault(d => d.Id.Equals(key));
if (enterpriseCustomer == null)
{
return NotFound();
}
return enterpriseCustomer.CreditLimit;
}
The expected response is shown below:
4200
Retrieving the raw value of the number of items in a collection-valued property
To retrieve the raw value of the number of items in a collection-valued property, append /$count
to that property's URL.
The route templates for this request are:
GET ~/{entityset}({key})/{collectionvaluedproperty}/$count
GET ~/{entityset}/{key}/{collectionvaluedproperty}/$count
GET ~/{singleton}/{collectionvaluedproperty}/$count
The following request returns the raw value of the number of items in the ContactPhones
collection-valued property on customer 1:
GET http://localhost:5000/odata/Customers(1)/ContactPhones/$count
For the above request to be conventionally-routed, a controller action named GetContactPhones
that accepts the key parameter is expected. The action should be decorated with EnableQuery
attribute. The EnableQuery
attribute is responsible for generating the relevant query for determining the number of items:
[EnableQuery]
public ActionResult<IEnumerable<string>> GetContactPhones([FromRoute] int key)
{
var customer = customers.SingleOrDefault(d => d.Id.Equals(key));
if (customer == null)
{
return NotFound();
}
return customer.ContactPhones;
}
The expected response is shown below:
1
Add an item to a collection-valued property
The route templates for this request are:
POST ~/{entityset}({key})/{collectionvaluedproperty}
POST ~/{entityset}/{key}/{collectionvaluedproperty}
POST ~/{singleton}/{collectionvaluedproperty}
The following POST
request adds an item to the ContactPhones
primitive collection-valued property on customer 1:
POST http://localhost:5000/odata/Customers(1)/ContactPhones
Here's the request body:
{
"value": "798-507-2014"
}
The payload for adding an item to a primitive collection-valued property should be a JSON object containing a single property named value
.
For the above request to be conventionally-routed, a controller action named PostToContactPhones
is expected. The controller action should accept two parameters - the first is the key parameter and the second a parameter of type string
decorated with FromBody
attribute:
public ActionResult PostToContactPhones([FromRoute] int key, [FromBody] string contactPhone)
{
var customer = customers.SingleOrDefault(d => d.Id.Equals(key));
if (customer == null)
{
return NotFound();
}
customer.ContactPhones.Add(contactPhone);
return Accepted();
}
The response status code should be 201 Created
. You can query for the entity to confirm the result of the POST
operation.
Adding an item to a collection-valued property declared on a derived type
The route templates for this request are:
POST ~/{entityset}({key})/{cast}/{collectionvaluedproperty}
POST ~/{entityset}/{key}/{cast}/{collectionvaluedproperty}
POST ~/{singleton}/{cast}/{collectionvaluedproperty}
The following POST
request adds an item to the ShippingAddresses
complex collection-valued property on enterprise customer 3. The ShippingAddresses
property is declared on the EnterpriseCustomer
derived type:
POST http://localhost:5000/odata/Customers(3)/PropertyRouting.Models.EnterpriseCustomer/ShippingAddresses
Here's the request body:
{
"Street": "One Microsoft Way"
}
Unlike adding an item to a primitive collection-valued property, the payload for adding an item to a complex collection-valued property is a JSON object of the complex type.
For the above request to be conventionally-routed, a controller action named PostToShippingAddressesFromEnterpriseCustomer
is expected. The controller action should accept two parameters - the first is the key parameter and the second a parameter of type Address
decorated with FromBody
attribute:
public ActionResult PostToShippingAddressesFromEnterpriseCustomer([FromRoute] int key, [FromBody] Address address)
{
var enterpriseCustomer = customers.OfType<EnterpriseCustomer>().SingleOrDefault(d => d.Id.Equals(key));
if (enterpriseCustomer == null)
{
return NotFound();
}
enterpriseCustomer.ShippingAddresses.Add(address);
return Accepted();
}
The response status code should be 201 Created
. You can query for the entity to confirm the result of the POST
operation.
Updating a primitive property
The route templates for this request are:
PUT ~/{entityset}({key})/{property}
PUT ~/{entityset}/{key}/{property}
PUT ~/{singleton}/{property}
The following PUT
request updates the Name
primitive property on customer 1:
PUT http://localhost:5000/odata/Customers(1)/Name
Here's the request body:
{
"value": "Sue"
}
The payload for updating a primitive property should be a JSON object containing a single property named value
.
For the above request to be conventionally-routed, a controller action named PutToName
is expected. The controller action should accept two parameters - the first is the key parameter and the second a parameter of type string
decorated with FromBody
attribute:
public ActionResult PutToName([FromRoute] int key, [FromBody] string name)
{
var customer = customers.SingleOrDefault(d => d.Id.Equals(key));
if (customer == null)
{
return NotFound();
}
customer.Name = name;
return Ok();
}
The response status code should be 200 OK
. You can query for the entity to confirm the result of the PUT
operation.
Updating a complex property
The route templates for this request are:
PUT ~/{entityset}({key})/{property}
PUT ~/{entityset}/{key}/{property}
PUT ~/{singleton}/{property}
The following PUT
request updates the BillingAddress
complex property on customer 1:
PUT http://localhost:5000/odata/Customers(1)/BillingAddress
Here's the request body:
{
"Street": "One Microsoft Way"
}
Unlike updating a primitive property, the payload for updating a complex property is a JSON object of the complex type.
For the above request to be conventionally-routed, a controller action named PutToBillingAddress
is expected. The controller action should accept two parameters - the first is the key parameter and the second a parameter of type Address
decorated with FromBody
attribute:
public ActionResult PutToBillingAddress([FromRoute] int key, [FromBody] Address address)
{
var customer = customers.SingleOrDefault(d => d.Id.Equals(key));
if (customer == null)
{
return NotFound();
}
customer.BillingAddress = address;
return Ok();
}
The response status code should be 200 OK
. You can query for the entity to confirm the result of the PUT
operation.
Updating a collection-valued property
The route templates for this request are:
PUT ~/{entityset}({key})/{property}
PUT ~/{entityset}/{key}/{property}
PUT ~/{singleton}/{property}
The following PUT
request updates the ContactPhones
collection-valued property on customer 1:
PUT http://localhost:5000/odata/Customers(1)/ContactPhones
Here's the request body:
{
"value": ["804-855-4049", "491-9198-476"]
}
For the above request to be conventionally-routed, a controller action named PutToContactPhones
is expected. The controller action should accept two parameters - the first is the key parameter and the second a parameter of type IEnumerable<string>
decorated with FromBody
attribute:
public ActionResult PutToContactPhones([FromRoute] int key, [FromBody] IEnumerable<string> contactPhones)
{
var customer = customers.SingleOrDefault(d => d.Id.Equals(key));
if (customer == null)
{
return NotFound();
}
customer.ContactPhones = contactPhones?.ToList();
return Ok();
}
The use of IEnumerable<T>
as the parameter type is not accidental; other concrete generic types like List
or Collection
will cause an InvalidCastException
to be thrown.
The response status code should be 200 OK
. You can query for the entity to confirm the result of the PUT
operation.
Updating a property declared on a derived type
The route templates for this request are:
PUT ~/{entityset}({key})/{cast}/{property}
PUT ~/{entityset}/{key}/{cast}/{property}
PUT ~/{singleton}/{cast}/{property}
The following PUT
request updates the RegisteredAddress
property on enterprise customer 3:
PUT http://localhost:5000/odata/Customers(3)/PropertyRouting.Models.EnterpriseCustomer/RegisteredAddress
Here's the request body:
{
"Street": "One Microsoft Way"
}
For the above request to be conventionally-routed, a controller action named PutToRegisteredAddressFromEnterpriseCustomer
is expected. The controller action should accept two parameters - the first is the key parameter and the second a parameter of type Address
decorated with FromBody
attribute:
public ActionResult PutToRegisteredAddressFromEnterpriseCustomer([FromRoute] int key, [FromBody] Address address)
{
var enterpriseCustomer = customers.OfType<EnterpriseCustomer>().SingleOrDefault(d => d.Id.Equals(key));
if (enterpriseCustomer == null)
{
return NotFound();
}
enterpriseCustomer.RegisteredAddress = address;
return Ok();
}
The response status code should be 200 OK
. You can query for the entity to confirm the result of the PUT
operation.
Updating a property declared on a derived type and set to an instance of a derived type
The route templates for this request are:
PUT ~/{entityset}({key})/{cast}/{property}/{cast}
PUT ~/{entityset}/{key}/{cast}/{property}/{cast}
PUT ~/{singleton}/{cast}/{property}/{cast}
The following PUT
request updates the RegisteredAddress
property on enterprise customer 4. The property value is an instance of PostalAddress
derived type:
PUT http://localhost:5000/odata/Customers(4)/PropertyRouting.Models.EnterpriseCustomer/RegisteredAddress/PropertyRouting.Models.PostalAddress
Here's the request body:
{
"Street": "One Microsoft Way",
"PostalCode": "98052"
}
For the above request to be conventionally-routed, a controller action named PutToRegisteredAddressOfPostalAddressFromEnterpriseCustomer
is expected. The controller action should accept two parameters - the first is the key parameter and the second a parameter of type PostalAddress
decorated with FromBody
attribute:
public ActionResult PutToRegisteredAddressOfPostalAddressFromEnterpriseCustomer([FromRoute] int key, [FromBody] PostalAddress registeredAddress)
{
var enterpriseCustomer = customers.OfType<EnterpriseCustomer>().SingleOrDefault(d => d.Id.Equals(key));
if (enterpriseCustomer == null)
{
return NotFound();
}
enterpriseCustomer.RegisteredAddress = registeredAddress;
return Ok();
}
The response status code should be 200 OK
. You can query for the entity to confirm the result of the PUT
operation.
Alternatively, the PutToRegisteredAddressFromEnterpriseCustomer
controller action defined in updating a property declared on a derived type section can be used to achieve the same objective. The request below would be routed to PutToRegisteredAddressFromEnterpriseCustomer
controller action:
PUT http://localhost:5000/odata/Customers(3)/PropertyRouting.Models.EnterpriseCustomer/RegisteredAddress
Here's the request body. The @odata.type
annotation is used to specify that the payload is a PostalAddress
:
{
"@odata.type": "#PropertyRouting.Models.PostalAddress",
"Street": "One Microsoft Way",
"PostalCode": "98052"
}
Patching a complex property
You can only patch single-valued complex properties.
The route templates for this request are:
PATCH ~/{entityset}({key})/{singlevaluedproperty}
PATCH ~/{entityset}/{key}/{singlevaluedproperty}
PATCH ~/{singleton}/{singlevaluedproperty}
The following PATCH
request patches the BillingAddress
property on customer 1:
PATCH http://localhost:5000/odata/Customers(1)/BillingAddress
Here's the request body:
{
"Street": "One Microsoft Way"
}
For the above request to be conventionally-routed, a controller action named PatchToBillingAddress
is expected. The controller action should accept two parameters - the first is the key parameter and the second a parameter of type Delta<Address>
decorated with FromBody
attribute:
public ActionResult PatchToBillingAddress([FromRoute] int key, [FromBody] Delta<Address> delta)
{
var customer = customers.SingleOrDefault(d => d.Id.Equals(key));
if (customer == null)
{
return NotFound();
}
delta.Patch(customer.BillingAddress);
return Ok();
}
The response status code should be 200 OK
. You can query for the entity to confirm the result of the PATCH
operation.
Patching a complex property set to an instance of a derived type
The route templates for this request are:
PATCH ~/{entityset}({key})/{singlevaluedproperty}/{cast}
PATCH ~/{entityset}/{key}/{singlevaluedproperty}/{cast}
PATCH ~/{singleton}/{singlevaluedproperty}/{cast}
The following PATCH
request patches the BillingAddress
property on customer 2. The property value is an instance of PostalAddress
derived type:
PATCH http://localhost:5000/odata/Customers(2)/BillingAddress/PropertyRouting.Models.PostalAddress
Here's the request body:
{
"Street": "One Microsoft Way",
"PostalCode": "98052"
}
For the above request to be conventionally-routed, a controller action named PatchToBillingAddressOfPostalAddress
is expected. The controller action should accept two parameters - the first is the key parameter and the second a parameter of type Delta<PostalAddress>
decorated with FromBody
attribute:
public ActionResult PatchToBillingAddressOfPostalAddress([FromRoute] int key, [FromBody] Delta<PostalAddress> delta)
{
var customer = customers.SingleOrDefault(d => d.Id.Equals(key));
if (!(customer?.BillingAddress is PostalAddress billingAddress))
{
return NotFound();
}
delta.Patch(billingAddress);
return Ok();
}
The response status code should be 200 OK
. You can query for the entity to confirm the result of the PATCH
operation.
Alternatively, the PatchToBillingAddress
controller action defined in patching a complex property section can be used to achieve the same objective. The request below would be routed to PatchToBillingAddress
controller action:
PATCH http://localhost:5000/odata/Customers(2)/BillingAddress
Here's the request body. The @odata.type
annotation is used to specify that the payload is a PostalAddress
:
{
"@odata.type": "#PropertyRouting.Models.PostalAddress",
"Street": "One Microsoft Way",
"PostalCode": "98052"
}
Patching a complex property declared on a derived type
The route templates for this request are:
PATCH ~/{entityset}({key})/{cast}/{singlevaluedproperty}
PATCH ~/{entityset}/{key}/{cast}/{singlevaluedproperty}
PATCH ~/{singleton}/{cast}/{singlevaluedproperty}
The following PATCH
request patches the RegisteredAddress
property on enterprise customer 3:
PATCH http://localhost:5000/odata/Customers(3)/PropertyRouting.Models.EnterpriseCustomer/RegisteredAddress
Here's the request body:
{
"Street": "One Microsoft Way"
}
For the above request to be conventionally-routed, a controller action named PatchToRegisteredAddressFromEnterpriseCustomer
is expected. The controller action should accept two parameters - the first is the key parameter and the second a parameter of type Delta<Address>
decorated with FromBody
attribute:
public ActionResult PatchToRegisteredAddressFromEnterpriseCustomer([FromRoute] int key, [FromBody] Delta<Address> delta)
{
var enterpriseCustomer = customers.OfType<EnterpriseCustomer>().SingleOrDefault(d => d.Id.Equals(key));
if (enterpriseCustomer == null)
{
return NotFound();
}
delta.Patch(enterpriseCustomer.RegisteredAddress);
return Ok();
}
The response status code should be 200 OK
. You can query for the entity to confirm the result of the PATCH
operation.
Patching a complex property declared on a derived type and set to an instance of a derived type
The route templates for this request are:
PATCH ~/{entityset}({key})/{cast}/{singlevaluedproperty}/{cast}
PATCH ~/{entityset}/{key}/{cast}/{singlevaluedproperty}/{cast}
PATCH ~/{singleton}/{cast}/{singlevaluedproperty}/{cast}
The following PATCH
request patches the RegisteredAddress
property on enterprise customer 4. The property value is an instance of PostalAddress
derived type:
PATCH http://localhost:5000/odata/Customers(4)/PropertyRouting.Models.EnterpriseCustomer/RegisteredAddress/PropertyRouting.Models.PostalAddress
Here's the request body:
{
"Street": "One Microsoft Way",
"PostalCode": "98052"
}
For the above request to be conventionally-routed, a controller action named PatchToRegisteredAddressOfPostalAddressFromEnterpriseCustomer
is expected. The controller action should accept two parameters - the first is the key parameter and the second a parameter of type Delta<PostalAddress>
decorated with FromBody
attribute:
public ActionResult PatchToRegisteredAddressOfPostalAddressFromEnterpriseCustomer([FromRoute] int key, [FromBody] Delta<PostalAddress> delta)
{
var enterpriseCustomer = customers.OfType<EnterpriseCustomer>().SingleOrDefault(d => d.Id.Equals(key));
if (!(enterpriseCustomer?.RegisteredAddress is PostalAddress registeredAddress))
{
return NotFound();
}
delta.Patch(registeredAddress);
return Ok();
}
The response status code should be 200 OK
. You can query for the entity to confirm the result of the PATCH
operation.
Alternatively, the PatchToRegisteredAddressFromEnterpriseCustomer
controller action defined in patching a complex property declared on a derived type section can be used to achieve the same objective. The request below would be routed to PatchToRegisteredAddressFromEnterpriseCustomer
controller action:
PATCH http://localhost:5000/odata/Customers(4)/PropertyRouting.Models.EnterpriseCustomer/RegisteredAddress
Here's the request body. The @odata.type
annotation is used to specify that the payload is a PostalAddress
:
{
"@odata.type": "#PropertyRouting.Models.PostalAddress",
"Street": "One Microsoft Way",
"PostalCode": "98052"
}
Deleting a nullable property
You can only delete a nullable primitive or complex property. Deleting a property involves setting its value to null
.
The route templates for this request are:
DELETE ~/{entityset}({key})/{nullableproperty}
DELETE ~/{entityset}/{key}/{nullableproperty}
DELETE ~/{singleton}/{nullableproperty}
The following DELETE
request deletes the BillingAddress
property on customer 1:
DELETE http://localhost:5000/odata/Customers(1)/BillingAddress
The request body is empty.
For the above request to be conventionally-routed, a controller action named DeleteToBillingAddress
that accepts the key parameter is expected:
public ActionResult DeleteToBillingAddress([FromRoute] int key)
{
var customer = customers.SingleOrDefault(d => d.Id.Equals(key));
if (customer == null)
{
return NotFound();
}
customer.BillingAddress = null;
return NoContent();
}
The response status code should be 204
. You can query for the entity to confirm the result of the DELETE
operation.
Deleting a nullable property declared on a derived type
The route templates for this request are:
DELETE ~/{entityset}({key})/{cast}/{nullableproperty}
DELETE ~/{entityset}/{key}/{cast}/{nullableproperty}
DELETE ~/{singleton}/{cast}/{nullableproperty}
The following DELETE
request deletes the RegisteredAddress
property on customer 3:
DELETE http://localhost:5000/odata/Customers(3)/PropertyRouting.Models.EnterpriseCustomer/RegisteredAddress
The request body is empty.
For the above request to be conventionally-routed, a controller action named DeleteToRegisteredAddressFromEnterpriseCustomer
that accepts the key parameter is expected:
public ActionResult DeleteToRegisteredAddressFromEnterpriseCustomer([FromRoute] int key)
{
var enterpriseCustomer = customers.OfType<EnterpriseCustomer>().SingleOrDefault(d => d.Id.Equals(key));
if (enterpriseCustomer == null)
{
return NotFound();
}
enterpriseCustomer.RegisteredAddress = null;
return NoContent();
}
The response status code should be 204
. You can query for the entity to confirm the result of the DELETE
operation.
Property routing endpoint mappings
If you went through this tutorial and implemented the logic in an OData service, you can run the application and visit the $odata
endpoint (http://localhost:5000/$odata) to view the endpoint mappings.
Feedback
https://aka.ms/ContentUserFeedback.
Coming soon: Throughout 2024 we will be phasing out GitHub Issues as the feedback mechanism for content and replacing it with a new feedback system. For more information see:Submit and view feedback for