Action 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 action 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. You may also want to go through the Actions & Functions tutorial to get a better understanding of Edm actions.
Introduction
Actions are a way to add server-side logic that is not easily defined as CRUD (Create, Read, Update, and Delete) operations on entities. An action can have side effects, which is why they are invoked with the HTTP POST method. An action can target a single entity or a collection of entities. In OData terminology, this is binding. You can also have "unbound" actions, which are basically static operations on the OData service.
OData action routing convention supports the following route templates:
Request Method | Route Template |
---|---|
POST |
~/{entityset}\|{singleton}/{action} |
POST |
~/{entityset}\|{singleton}/{cast}/{action} |
POST |
~/{entityset}/{key}/{action} |
POST |
~/{entityset}/{key}/{cast}/{action} |
Notes:
- OData routing supports canonical parentheses-style key (e.g.
~/Employees(1)
) in addition to key-as-segment (e.g.~/Employees/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 action 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:
Employee
class
namespace ActionRouting.Models
{
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public string SwagGift { get; set; }
}
}
Manager
class
namespace ActionRouting.Models
{
public class Manager : Employee
{
public decimal Bonus { 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();
var employeeEntityType = modelBuilder.EntitySet<Employee>("Employees").EntityType;
var managerEntityType = modelBuilder.EntityType<Manager>();
employeeEntityType.Collection.Action("ConferSwagGifts")
.Parameter<string>("SwagGift");
employeeEntityType.Action("ConferSwagGift")
.Parameter<string>("SwagGift");
managerEntityType.Collection.Action("ConferBonuses")
.Parameter<decimal>("Bonus");
managerEntityType.Action("ConferBonus")
.Parameter<decimal>("Bonus");
builder.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 Employees
. Implicitly, Employee
and Manager
get included in the Edm model as entity types.
Four Edm actions are also defined:
ConferSwagGifts
- bound to theEmployee
entity collectionConferSwagGift
- bound to theEmployee
entityConferBonuses
- bound to theManager
derived entity collectionConferBonus
- bound to theManager
derived entity
Controller
The partial structure of the controller for the OData service is as follows:
using System;
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 ActionRouting.Models;
public class EmployeesController : ODataController
{
private static List<Employee> employees = new List<Employee>
{
new Employee { Id = 1, Name = "Employee 1" },
new Employee { Id = 2, Name = "Employee 2" },
new Employee { Id = 3, Name = "Employee 3" },
new Employee { Id = 4, Name = "Employee 4" },
new Manager { Id = 5, Name = "Employee 5" },
new Manager { Id = 6, Name = "Employee 6" }
};
}
Routing bound Edm actions
In this section we cover the conventions for routing bound actions and the controller actions (endpoints) required for the requests to be routed successfully.
When invoking an Edm action, the body of the request contains the action parameters as a JSON payload. ASP.NET Core automatically converts the JSON payload to an ODataActionParameters
object, which is basically a dictionary of parameter values. You use this dictionary to access the parameters in your controller action.
Invoking an action bound to an entity set or singleton
The route template for this request is: POST ~/{entityset}|{singleton}/{action}
.
The following request invokes the ConferSwagGifts
action bound to the Employees
entity set. The URL for the action is the action name appended to the entity set's URL:
POST http://localhost:5000/odata/Employees/ConferSwagGifts
An alternative URL for invoking the same action is:
POST http://localhost:5000/odata/Employees/Default.ConferSwagGifts
The Edm organizes elements into a hierarchy. Based on our Edm model, ConferSwagGifts
action is under a schema element with Default
as the namespace name.
Here's the request body:
{
"SwagGift": "Mug"
}
For the above request to be conventionally-routed, a controller action named ConferSwagGifts
that accepts a single parameter of type ODataActionParameters
is expected. The action should be decorated with HttpPost
attribute:
[HttpPost]
public ActionResult ConferSwagGifts(ODataActionParameters parameters)
{
if (parameters != null && parameters.TryGetValue("SwagGift", out object swag))
{
foreach (var employee in employees)
{
employee.SwagGift = Convert.ToString(swag);
}
}
else
{
return BadRequest();
}
return Ok();
}
The response status code should be 200
. Querying any of the employees should confirm that the SwagGift
property is updated with Mug
- a relevant controller action to support retrieving a single entity would need to be implemented for this to work.
Invoking an action bound to an entity
The route templates for this request are:
POST ~/{entityset}({key})/{action}
POST ~/{entityset}/{key}/{action}
The following request invokes the ConferSwagGift
action bound to employee 1. The URL for the action is the action name appended to the entity's URL:
POST http://localhost:5000/odata/Employees(1)/ConferSwagGift
An alternative URL for invoking the same action is:
POST http://localhost:5000/odata/Employees(1)/Default.ConferSwagGift
Here's the request body:
{
"SwagGift": "Gaiter"
}
For the above request to be conventionally-routed, a controller action named ConferSwagGift
is expected. The controller action should accept two parameters - the first is the key parameter and the second a parameter of type ODataActionParameters
. The action should be decorated with HttpPost
attribute:
[HttpPost]
public ActionResult ConferSwagGift([FromRoute] int key, ODataActionParameters parameters)
{
if (parameters != null && parameters.TryGetValue("SwagGift", out object swag))
{
var employee = employees.SingleOrDefault(d => d.Id.Equals(key));
if (employee == null)
{
return NotFound();
}
employee.SwagGift = Convert.ToString(swag);
}
else
{
return BadRequest();
}
return Ok();
}
The response status code should be 200
. Querying employee 1 should confirm that the SwagGift
property is updated with Gaiter
- a relevant controller action to support retrieving a single entity would need to be implemented for this to work.
Invoking an action bound to a collection of derived entities or derived singleton
The route template for this request is: POST ~/{entityset}|{singleton}/{cast}/{action}
.
The following request invokes the ConferBonuses
action bound to a Manager
derived entity collection. The URL for the action is the action name appended to the collection of derived entities' URL:
POST http://localhost:5000/odata/Employees/ActionRouting.Models.Manager/ConferBonuses
An alternative URL for invoking the same action is:
POST http://localhost:5000/odata/Employees/ActionRouting.Models.Manager/Default.ConferBonuses
Here's the request body:
{
"Bonus": 130
}
For the above request to be conventionally-routed, a controller action named ConferBonusesOnCollectionOfManager
that accepts a single parameter of type ODataActionParameters
is expected. The action should be decorated with HttpPost
attribute:
[HttpPost]
public ActionResult ConferBonusesOnCollectionOfManager(ODataActionParameters parameters)
{
if (parameters != null && parameters.TryGetValue("Bonus", out object bonus))
{
var managers = employees.OfType<Manager>();
foreach (var manager in managers)
{
manager.Bonus = Convert.ToDecimal(bonus);
}
}
else
{
return BadRequest();
}
return Ok();
}
The response status code should be 200
. Querying any of the managers should confirm that the Bonus
property is updated with 130
- a relevant controller action to support retrieving a single derived entity would need to be implemented for this to work.
Invoking an action bound to a derived entity
The route templates for this request are:
POST ~/{entityset}({key})/{cast}/{action}
POST ~/{entityset}/{key}/{cast}/{action}
The following request invokes the ConferBonus
action bound to employee 5 (a manager). The URL for the action is the action name appended to the derived entity's URL:
POST http://localhost:5000/odata/Employees(5)/ActionRouting.Models.Manager/ConferBonus
An alternative URL for invoking the same action is:
POST http://localhost:5000/odata/Employees(5)/ActionRouting.Models.Manager/Default.ConferBonus
Here's the request body:
{
"Bonus": 70
}
For the above request to be conventionally-routed, a controller action named ConferBonusOnManager
is expected. The controller action should accept two parameters - the first is the key parameter and the second a parameter of type ODataActionParameters
. The action should be decorated with HttpPost
attribute:
[HttpPost]
public ActionResult ConferBonusOnManager([FromRoute] int key, ODataActionParameters parameters)
{
if (parameters != null && parameters.TryGetValue("Bonus", out object bonus))
{
var manager = employees.OfType<Manager>().SingleOrDefault(d => d.Id.Equals(key));
if (manager == null)
{
return NotFound();
}
manager.Bonus = Convert.ToDecimal(bonus);
}
else
{
return BadRequest();
}
return Ok();
}
The response status code should be 200
. Querying employee 5 (a manager) should confirm that the Bonus
property is updated with 70
- a relevant controller action to support retrieving a single derived entity would need to be implemented for this to work.
Routing unbound Edm actions
In this section we cover routing of unbound actions and the controller actions (endpoints) required for the requests to be routed successfully.
The following code configures an unbound action named ComputeSalary
in the Edm model. The action accepts two action parameters in the request body, namely, hourlyRate
and hoursWorked
, and returns a decimal
result. Notice that we call Action
directly on the ODataModelBuilder
, instead of entity type or collection:
var computeSalaryFunction = modelBuilder.Action("ComputeSalary");
computeSalaryFunction.Parameter<decimal>("hourlyRate");
computeSalaryFunction.Parameter<int>("hoursWorked");
computeSalaryFunction.Returns<decimal>();
An unbound action can be placed in any controller in the application. To avoid confusion, you can create a controller unassociated with any entity set to serve as a home for your unbound operations.
In ASP.NET Core, the default route is one where the route prefix is an empty string or null
. To associate an unbound action with a configured non-default route, the route template on the controller action should start with the route prefix:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Formatter;
using Microsoft.AspNetCore.OData.Routing.Controllers;
public class DefaultController : ODataController
{
[HttpPost("odata/ComputeSalary")]
public ActionResult<decimal> ComputeSalary(ODataActionParameters parameters)
{
object hourlyRateAsObject, hoursWorkedAsObject;
decimal hourlyRate;
int hoursWorked;
if (parameters == null
|| !parameters.TryGetValue("hourlyRate", out hourlyRateAsObject)
|| !decimal.TryParse(hourlyRateAsObject.ToString(), out hourlyRate)
|| !parameters.TryGetValue("hoursWorked", out hoursWorkedAsObject)
|| !int.TryParse(hoursWorkedAsObject.ToString(), out hoursWorked))
{
return BadRequest();
}
return hourlyRate * hoursWorked;
}
}
The following request invokes the ComputeSalary
unbound action:
POST http://localhost:5000/odata/ComputeSalary
Here's the request body:
{
"hourlyRate": 17.0,
"hoursWorked": 40
}
The following JSON payload shows the expected response:
{
"@odata.context": "http://localhost:5000/odata/$metadata#Edm.Decimal",
"value": 680.0
}
Action 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