EntitySet 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 entity set 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
Entity sets are named collections of entities (e.g. Customers
is an entity set containing Customer
entities). Entity sets provide the primary entry points into the data model.
OData entity set routing convention supports the following route templates:
Request Method | Route Template |
---|---|
GET |
~/{entityset} |
GET |
~/{entityset}/$count |
GET |
~/{entityset}/{cast} |
GET |
~/{entityset}/{cast}/$count |
POST |
~/{entityset} |
POST |
~/{entityset}/{cast} |
PATCH |
~/{entityset} |
PATCH |
~/{entityset}/{cast} |
Note: {cast}
is a placeholder for the fully-qualified name for a derived type
To illustrate entity set 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:
Shape
class
namespace EntitySetRouting.Models
{
public class Shape
{
public int Id { get; set; }
public double Area { get;set; }
}
}
Rectangle
class
namespace EntitySetRouting.Models
{
public class Rectangle : Shape
{
public double Length { get; set; }
public double Width { get; set; }
}
}
Circle
class
namespace EntitySetRouting.Models
{
public class Circle : Shape
{
public double Radius { 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 EntitySetRouting.Models;
var builder = WebApplication.CreateBuilder(args);
var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Shape>("Shapes");
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 Shapes
. Implicitly, Shape
, Rectangle
and Circle
get included in the Edm model as entity types.
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 EntitySetRouting.Models;
public class ShapesController : ODataController
{
private static List<Shape> shapes = new List<Shape>
{
new Rectangle { Id = 1, Length = 7, Width = 4, Area = 28 },
new Circle { Id = 2, Radius = 3.5, Area = 38.5 },
new Rectangle { Id = 3, Length = 8, Width = 5, Area = 40 }
};
}
Routing conventions for entity sets
In this section we cover the conventions for entity set routing and the controller actions (endpoints) required for the requests to be routed successfully.
Retrieving an entity set
The route template for this request is: GET ~/{entityset}
.
The following request returns the Shapes
entity set - basically a collection of Shape
entities:
GET http://localhost:5000/odata/Shapes
For the above request to be conventionally-routed, a controller action named Get
(or GetShapes
) is expected:
public ActionResult<IEnumerable<Shape>> Get()
{
return shapes;
}
The following JSON payload shows the expected response:
{
"@odata.context": "http://localhost:5000/odata/$metadata#Shapes",
"value": [
{
"@odata.type": "#EntitySetRouting.Models.Rectangle",
"Id": 1,
"Area": 28.0,
"Length": 7.0,
"Width": 4.0
},
{
"@odata.type": "#EntitySetRouting.Models.Circle",
"Id": 2,
"Area": 38.5,
"Radius": 3.5
},
{
"@odata.type": "#EntitySetRouting.Models.Rectangle",
"Id": 3,
"Area": 40.0,
"Length": 8.0,
"Width": 5.0
}
]
}
The response contains 3 shape objects - 2 rectangles and 1 circle. Since Rectangle
and Circle
are derived types, each of the shape objects contain an @odata.type
property specifying the type of the entity.
Retrieving the count of an entity set
The route template for this request is: GET ~/{entityset}/$count
.
To address the raw value of the number of items in an entity set, append /$count
to that entity set's URL.
The following request returns a count of items in the Shapes
entity set:
GET http://localhost:5000/odata/Shapes/$count
For the above request to be conventionally-routed, a controller action named Get
(or GetShapes
) is expected, same as is expected when retrieving an entity set. However, the controller action needs to 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<Shape>> Get()
{
return shapes;
}
The expected response is shown below:
3
Retrieving a collection of derived entities
The route template for this request is: GET ~/{entityset}/{cast}
.
The following request returns a collection of derived Rectangle
entities:
GET http://localhost:5000/odata/Shapes/EntitySetRouting.Models.Rectangle
For the above request to be conventionally-routed, a controller action named GetFromRectangle
(or GetShapesFromRectangle
) is expected:
public ActionResult<IEnumerable<Rectangle>> GetFromRectangle()
{
return shapes.OfType<Rectangle>().ToList();
}
The following JSON payload shows the expected response:
{
"@odata.context": "http://localhost:5000/odata/$metadata#Shapes/EntitySetRouting.Models.Rectangle",
"value": [
{
"Id": 1,
"Area": 28.0,
"Length": 7.0,
"Width": 4.0
},
{
"Id": 3,
"Area": 40.0,
"Length": 8.0,
"Width": 5.0
}
]
}
The response contains 2 rectangle objects.
Retrieving the count of a collection of derived entities
The route template for this request is: GET ~/{entityset}/{cast}/$count
.
The following request returns a count of items in the Shapes
entity set that are rectangles:
GET http://localhost:5000/odata/Shapes/EntitySetRouting.Models.Rectangle/$count
For the above request to be conventionally-routed, a controller action named GetFromRectangle
(or GetShapesFromRectangle
) is expected, same as is expected when retrieving a collection of derived entities. However, the controller action needs to be decorated with EnableQuery
attribute:
[EnableQuery]
public ActionResult<IEnumerable<Rectangle>> GetFromRectangle()
{
return shapes.OfType<Rectangle>().ToList();
}
The expected response is shown below:
2
Adding an entity to an entity set
To create an entity in an entity set, the client sends a POST
request to that entity set's URL.
The route template for this request is: POST ~/{entityset}
The following POST
request adds a Shape
entity to the Shapes
entity set:
POST http://localhost:5000/odata/Shapes
Here's the request body:
{
"Id": 4,
"Area": 36
}
For the above request to be conventionally-routed, a controller action named Post
(or PostShape
) that accepts a parameter of type Shape
decorated with FromBody
attribute is expected:
public ActionResult Post([FromBody] Shape shape)
{
shapes.Add(shape);
return Created(shape);
}
The Created
method used in the above code block is defined in the ODataController
class - that ShapesController
derives from. The method generates the location at which the resource has been created and returns it as a response header.
The response status code should be 201
. The following JSON payload shows the expected response:
{
"@odata.context": "http://localhost:5000/odata/$metadata#Shapes/$entity",
"Id": 4,
"Area": 36.0
}
If you look through the response headers, you should find the location of the created entity: http://localhost:5000/odata/Shapes(4)
. Querying the Shapes
entity set should confirm that the entity was successfully added.
Adding a derived entity to an entity set
To create a derived entity in an entity set, the client sends a POST
request to that entity set's URL with the fully-qualified name of the derived type appended at the end.
The route template for this request is: POST ~/{entityset}/{cast}
The following POST
request adds a Circle
entity to the Shapes
entity set:
POST http://localhost:5000/odata/Shapes/EntitySetRouting.Models.Circle
Here's the request body:
{
"Id": 5,
"Radius": 1.4,
"Area": 6.16
}
For the above request to be conventionally-routed, a controller action named PostFromCircle
(or PostShapeFromCircle
) that accepts a parameter of type Circle
decorated with FromBody
attribute is expected:
public ActionResult PostFromCircle([FromBody] Circle circle)
{
shapes.Add(circle);
return Created(circle);
}
The response status code should be 201
. The following JSON payload shows the expected response:
{
"@odata.context": "http://localhost:5000/odata/$metadata#Shapes/EntitySetRouting.Models.Circle/$entity",
"Id": 5,
"Area": 6.16,
"Radius": 1.4
}
If you look through the response headers, you should find the location of the created derived entity: http://localhost:5000/odata/Shapes(5)
. Querying the Shapes
entity set should confirm that the derived entity was successfully added.
Patching a collection of entities
The semantics of PATCH
is to merge the content in the request payload with the entity's or entities' current state, applying the update only to those components specified in the request body. To patch a collection of entities in an entity set, the client sends a PATCH
request to that entity set's URL.
The route template for this request is: PATCH ~/{entityset}
The following PATCH
request patches shape 1 (a circle) and shape 2 (a rectangle):
PATCH http://localhost:5000/odata/Shapes
Here's the request body:
{
"value": [
{
"@odata.type": "#EntitySetRouting.Models.Circle",
"Id": 2,
"Radius": 0.7,
"Area": 1.54
},
{
"@odata.type": "#EntitySetRouting.Models.Rectangle",
"Id": 3,
"Length": 8,
"Width": 4,
"Area": 32
}
]
}
For the above request to be conventionally-routed, a controller action named Patch
(or PatchShape
) that accepts a parameter of type DeltaSet<Shape>
decorated with FromBody
attribute is expected:
public ActionResult Patch([FromBody] DeltaSet<Shape> deltaSet)
{
foreach (Delta<Shape> delta in deltaSet)
{
if (delta.TryGetPropertyValue("Id", out object idAsObject))
{
var shape = shapes.SingleOrDefault(d => d.Id.Equals(idAsObject));
delta.Patch(shape);
}
}
return Ok();
}
The response status code should be 200
. Querying Shapes
entity set should return the following:
{
"@odata.context": "http://localhost:44184/odata/$metadata#Shapes",
"value": [
{
"@odata.type": "#EntitySetRouting.Models.Rectangle",
"Id": 1,
"Area": 28.0,
"Length": 7.0,
"Width": 4.0
},
{
"@odata.type": "#EntitySetRouting.Models.Circle",
"Id": 2,
"Area": 1.54,
"Radius": 0.7
},
{
"@odata.type": "#EntitySetRouting.Models.Rectangle",
"Id": 3,
"Area": 32.0,
"Length": 8.0,
"Width": 4.0
}
]
}
Patching a collection of derived entities
To patch a collection of derived entities in an entity set, the client sends a PATCH
request to that entity set's URL with the fully-qualified name of the derived type appended at the end.
The route template for this request is: PATCH ~/{entityset}/{cast}
The following PATCH
request patches the rectangles with Id
value of 1 and 3 respectively:
PATCH http://localhost:5000/odata/Shapes/EntitySetRouting.Models.Rectangle
Here's the request body:
{
"value": [
{
"Id": 1,
"Length": 6,
"Width": 5,
"Area": 30
},
{
"Id": 3,
"Length": 8,
"Width": 4,
"Area": 32
}
]
}
For the above request to be conventionally-routed, a controller action named PatchFromRectangle
(or PatchShapeFromRectangle
) that accepts a parameter of type DeltaSet<Rectangle>
decorated with FromBody
attribute is expected:
public ActionResult PatchFromRectangle([FromBody] DeltaSet<Rectangle> deltaSet)
{
foreach (Delta<Rectangle> delta in deltaSet)
{
if (delta.TryGetPropertyValue("Id", out object idAsObject))
{
var rectangle = shapes.SingleOrDefault(d => d.Id.Equals(idAsObject)) as Rectangle;
delta.Patch(rectangle);
}
}
return Ok();
}
The response status code should be 200
. Querying Shapes
entity set should return the following:
{
"@odata.context": "http://localhost:44184/odata/$metadata#Shapes",
"value": [
{
"@odata.type": "#EntitySetRouting.Models.Rectangle",
"Id": 1,
"Area": 30.0,
"Length": 6.0,
"Width": 5.0
},
{
"@odata.type": "#EntitySetRouting.Models.Circle",
"Id": 2,
"Area": 38.5,
"Radius": 3.5
},
{
"@odata.type": "#EntitySetRouting.Models.Rectangle",
"Id": 3,
"Area": 32.0,
"Length": 8.0,
"Width": 4.0
}
]
}
Entity set 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: