Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Applies To:# OData Web API 8 supported
OData Web API v8
In OData, actions and functions are a way to add server-side behavior that is not easily defined as CRUD operations (Create-Read-Update-Delete) on entities. This documentation shows how to add actions and functions to an OData v4 endpoint, using ASP.NET Core OData v8.x.
What are actions and functions?
Functionsare operations exposed by an OData service that MUST return data and MUST have no observable side effects. They may support further composition. Functions are invoked by using HTTP GET requests.Actionsare operations exposed by an OData service that MAY have side effects when invoked. Actions have a side effect on the server, so they are invoked by using HTTP POST requests. They cannot be further composed in order to avoid non-deterministic behavior.
Side effects means that actions results in a change in the server; either a resource is updated, deleted or a new resource is created.
Bound and unbound operations.
From OData V4 spec, functions and actions can be either bound to a type or unbound. Bound operations are bound to an entity type, primitive type, complex type, or a collection. Unbound operations are invoked as static operations on the service.
URI patterns for calling OData actions and functions
In the examples below odata is the route prefix.
Bound function
The function is after an entity, entityset, singleton. Books and Graphs are entity sets.
GET ~/odata/Books/mostRecent()
GET ~/odata/Graphs/Default.GetShapeCount()
GET ~/odata/Graphs/Default.GetShapeCount(shapeType=FunctionActionBlog.ShapeType’Circle’)
GetShapeCount function has an overload that accepts a ShapeType parameter.
We can bind to a single entity.
GET ~/odata/Books(1)/isChildBook()
We can also bind to a singleton
GET ~/odata/Me/isMyCalendarBlocked()
Unbound function
Function is after the route prefix.
GET ~/odata/ReturnAllForKidsBooks
GET ~/odata/GetSalesTaxRate(10)
ReturnAllForKidsBooks doesn't have a parameter, while GetSalesTaxRate accepts one integer parameter.
If GetSalesTaxRate had more than one parameter, the call would be
GET ~/odata/GetSalesTaxRate(10, "US")
Parameter-less functions can work with or without parenthesis () after function names.
This is how we configure parameter-less functions to work without parenthesis.
builder.Services.AddControllers().AddOData(opt =>
{
opt.AddRouteComponents("odata", EdmModel.GetEdmModel()).Count().OrderBy().Filter().Select().Expand();
opt.RouteOptions.EnableNonParenthesisForEmptyParameterFunction = true;
});
Bound Action
Actions are invoked via POST requests therefore we can pass a payload in the body.
POST ~/odata/Books('1')/Rate
{
"rating": 7
}
Unbound action
POST ~/odata/incrementBookYear
{
"increment": 7,
"id": "1"
}
Practical examples
We will add detailed examples on how to configure bound and unbound functions and actions. We will start by setting up the ASP.NET Core OData v8.x project.
Data Model classes
public class Book
{
public string ID { get; set; }
public string Title { get; set; }
public bool ForKids { get; set; }
}
public class BookRating
{
public string ID { get; set; }
public int Rating { get; set; }
public string BookID { get; set; }
}
Controller
public class BooksController : ODataController
{
// Get ~/Books
[EnableQuery]
public IActionResult Get()
{
return Ok(DataSource.Instance.Books);
}
}
Build Edm Model
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Book>("books");
var model = builder.GetEdmModel();
return model;
}
Bound Function
Bound functions can be bound to an entity type or a collection of entity type. In our example model above, the bound function can be bound to a single book or a collection of books. We will demonstrate how to add a function that is bound to a collection.
In the Edm Model, we modify the code as follows.
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
// -------------
// Existing code
// -------------
builder.EntityType<Book>().Collection
.Function("mostRecent")
.Returns<string>();
var model = builder.GetEdmModel();
return model;
}
In the Edm Model above, we are adding a mostRecent function bound to the books collection. The function returns the ID of the most recent book. The ID is of string type.
We will update the BooksController as follows.
public class BooksController : ODataController
{
// -------------
// Existing code
// -------------
[HttpGet("odata/Books/mostRecent()")]
public IActionResult MostRecent()
{
var maxBookId = DataSource.Instance.Books.Max(x => x.ID);
return Ok(maxBookId);
}
}
If we invoke GET odata/Books/mostRecent()
We get the response below:
{
"@odata.context": "http://localhost:5000/odata/$metadata#Edm.String",
"value": "8"
}
Unbound function
Unbound functions don’t bind to any type and they are invoked as static operations. All unbound function overloads MUST have same return type.
In the Edm Model, we modify the code as follows.
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
// -------------
// Existing code
// -------------
builder.Function("returnAllForKidsBooks").ReturnsFromEntitySet<Book>("books");
var model = builder.GetEdmModel();
return model;
}
In the Edm Model above, we are adding a returnAllForKidsBooks function. The function returns a collection of books.
For parameterless functions, we can ignore the parenthesis.
We will update the BooksController as follows.
public class BooksController : ODataController
{
// -------------
// Existing code
// -------------
[HttpGet("odata/ReturnAllForKidsBooks")]
public IActionResult ReturnAllForKidsBooks()
{
var forKidsBooks = DataSource.Instance.Books.Where(m => m.ForKids == true);
return Ok(forKidsBooks);
}
}
If we invoke GET odata/ReturnAllForKidsBooks
We get the response below:
{
"@odata.context": "http://localhost:5000/odata/$metadata#books",
"value": [
{
"id": "2",
"isbn": "BB0011",
"title": "Book 2",
"year": 2001,
"forKids": true
},
{
"id": "4",
"isbn": "DD0011",
"title": "Book 4",
"year": 2003,
"forKids": true
},
{
"id": "5",
"isbn": "EE0011",
"title": "Book 5",
"year": 2004,
"forKids": true
},
{
"id": "6",
"isbn": "FF0011",
"title": "Book 6",
"year": 2005,
"forKids": true
},
{
"id": "7",
"isbn": "GG0011",
"title": "Book 7",
"year": 2006,
"forKids": true
}
]
}
Bound Action
Similar to bound functions, bound actions can be bound to entity type or collection of entity type. However, the overload of bound actions are different. Bound actions can be overloaded, but overload must happen by different binding parameter. For one binding parameter, there can be only one bound action.
In the Edm Model, we modify the code as follows.
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
// -------------
// Existing code
// -------------
builder.EntityType<Book>()
.Action("rate")
.Parameter<int>("rating");
var model = builder.GetEdmModel();
return model;
}
In the Edm Model above, we are adding a rate action which is bound to a Book entity type. The action takes a rating parameter of type int.
We will update the BooksController as follows.
public class BooksController : ODataController
{
// -------------
// Existing code
// -------------
[HttpPost("odata/Books({key})/Rate")]
public IActionResult Rate([FromODataUri] string key, ODataActionParameters parameters)
{
if (!ModelState.IsValid)
{
return BadRequest();
}
int rating = (int)parameters["rating"];
if (rating < 0)
{
return BadRequest();
}
return Ok(new BookRating() { BookID = key, Rating = rating });
}
}
If we invoke
POST odata/Books('1')/Rate
{"rating": 7}
We get the response below:
{
"id": null,
"rating": 7,
"bookID": "1"
}
Unbound Action
Unbound actions don’t bind to any type and they are invoked as static operations same as unbound functions. However, unbound actions do not allow overloading.
In the Edm Model, we modify the code as follows.
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
// -------------
// Existing code
// -------------
var action = builder.Action("incrementBookYear").ReturnsFromEntitySet<Book>("books");
action.Parameter<int>("increment");
action.Parameter<string>("id");
var model = builder.GetEdmModel();
return model;
}
In the Edm Model above, we are adding an incrementBookYear action. The action takes an increment parameter of type int and
an id parameter of type string.
We will update the BooksController as follows.
public class BooksController : ODataController
{
// -------------
// Existing code
// -------------
[HttpPost("odata/incrementBookYear")]
public IActionResult IncrementBookYear(ODataActionParameters parameters)
{
if (!ModelState.IsValid)
{
return BadRequest();
}
int increment = (int)parameters["increment"];
string bookId = (string)parameters["id"];
var book = DataSource.Instance.Books.Where(m => m.ID == bookId).FirstOrDefault();
if (book != null)
{
book.Year = book.Year + increment;
}
return Ok(book);
}
}
If we invoke
POST odata/incrementBookYear
{
"increment": 7,
"id": "1"
}
We get the response below:
{
"@odata.context": "http://localhost:5000/odata/$metadata#books/$entity",
"id": "1",
"isbn": "AA0011",
"title": "Book 1",
"year": 2002,
"forKids": false
}
Conclusion
In our examples above, we have provided very basic use cases for actions and functions. In real-world usage, actions and functions may contain complex logic to modify and get data across multiple entities.