An end-to-end OData v4 sample in .NET 8:
- OData.Sample.WebApi: an ASP.NET Core OData server with EF Core and SQLite, seeded with world regions, country regions and countries.
- OData.Sample.Client: a console app that uses an OData Connected Service proxy to run typed LINQ queries and CRUD operations against the server.
- Screenshots
- Architecture
- Repository layout
- Requirements
- Getting started
- The OData service
- Query examples
- The console client
- Tests
- Configuration
- Tech stack
- License
Swagger UI (Development profile only): the controllers under odata/v1 plus the plain AlleLaender controller for comparison.
Real requests against the locally running server (output condensed into tables). The $search term LaenderInEuropa is the custom search described below.
flowchart TB
client["OData.Sample.Client<br/>console app with 6 commands<br/>typed LINQ queries and CRUD"]
proxy["OData Connected Service proxy<br/>generated from $metadata"]
subgraph api["OData.Sample.WebApi (ASP.NET Core)"]
direction TB
odata["/odata/v1<br/>WorldRegions · CountryRegions · Countries<br/>$filter · $expand · $apply · $search"]
plain["/allelaender<br/>plain Web API controller"]
ef["EF Core"]
odata --> ef
plain --> ef
end
db[("SQLite<br/>ZSQLite.db")]
seed["data/*.json<br/>embedded seed data"]
client --> proxy
proxy -- "HTTPS, OData v4" --> odata
ef --> db
seed -. "wiped and reseeded at every start" .-> db
classDef ext fill:#eef2ff,stroke:#6366f1,color:#1e1b4b
classDef endpoint fill:#ecfdf5,stroke:#10b981,color:#064e3b
classDef data fill:#f1f5f9,stroke:#64748b,color:#0f172a
class client,proxy ext
class odata,plain,ef endpoint
class db,seed data
The client talks to the server only through the generated proxy; the server exposes the same data once as OData and once as a plain Web API controller for comparison.
| Path | Description |
|---|---|
data/ |
JSON seed data (WorldRegions, CountryRegions, Countries), embedded into the WebApi assembly |
doc/Help/ |
OData query options cheat sheet (PDF) |
doc/Postman/ |
Postman collections for this API and the public odata.org services |
shell/ |
ServerStart.cmd / ClientStart.cmd launch scripts |
src/WebApi/ |
OData server (OData.Sample.WebApi) |
src/Client/ |
Console client (OData.Sample.Client) |
ODataSample.sln |
Solution containing both projects |
- .NET 8 SDK
- EF Core CLI tools, needed once to create the database migration:
dotnet tool install --global dotnet-ef - Optional: Visual Studio 2022 or VS Code
- Optional: the OData Connected Service 2022+ extension, only needed if you want to regenerate the client proxy
dotnet build ODataSample.slnThe server applies EF Core migrations and reseeds the SQLite database at startup. The repository doesn't include a migrations folder, so create the initial migration once:
cd src/WebApi
dotnet ef migrations add InitialThe design-time factory (ODataSampleContextDesignFactory) reads its connection string from migrationsettings.json.
The server and client use HTTPS on localhost:
dotnet dev-certs https --trustdotnet run --project src/WebApiOn startup the server:
- applies migrations to
ZSQLite.dbin the WebApi folder, - deletes all existing rows and reseeds them from the embedded
data/*.jsonfiles.
All data changes are lost on every restart.
The server listens on:
| URL | Purpose |
|---|---|
https://localhost:7239/odata/v1 |
OData service root |
https://localhost:7239/odata/v1/$metadata |
CSDL metadata |
https://localhost:7239/$odata |
OData route debug page |
https://localhost:7239/swagger |
Swagger UI (Development only) |
HTTP is available on http://localhost:5239.
With the server running, in a second terminal:
dotnet run --project src/ClientThe scripts in shell/ do the same with --no-build, so build first and run them from inside shell/.
The EDM model (src/WebApi/Domain/EdmModel/CountriesEdmModel.cs) exposes three entity sets under odata/v1:
| Entity set | Navigation properties | Server page size |
|---|---|---|
WorldRegions |
CountryRegions, Countries |
10 |
CountryRegions |
WorldRegion, Countries |
10 |
Countries |
CountryRegion, WorldRegion |
none |
Each set supports GET (collection and by key), POST, PATCH/PUT (using Delta<T>) and DELETE.
All query options are enabled: $select, $expand, $filter, $orderby, $top, $skip, $count, $apply and $search.
CountrySearchBinder maps a few search terms on Countries to world-region filters:
| Term | World region |
|---|---|
LaenderInAfrika |
Africa |
LaenderInCaribic |
Caribbean |
LaenderInAmerika |
America |
LaenderInEuropa |
Europe |
LaenderInOzeanien |
Oceania |
LaenderInAsien |
Asia |
Any other single search term returns an error.
AlleLaenderController is a regular, non-OData ASP.NET Core controller at /allelaender and /allelaender/{id}. You can compare it with the OData Countries endpoint in Swagger.
Base URL: https://localhost:7239/odata/v1
# Select fields, filter, sort
/Countries?$select=ISO2,ISO3,Name&$filter=ISO2 in ('AT','DE','IT')&$orderby=ISO3
# Single entity by key
/Countries(40)
# Expand related entities, including nested expand
/Countries?$expand=WorldRegion,CountryRegion($expand=WorldRegion)&$filter=ISO2 eq 'AT'
# Filter with a string function, include total count
/Countries?$filter=startswith(Name,'A')&$count=true
# Count only
/Countries/$count
# Group and aggregate
/Countries?$apply=groupby((WorldRegionId),aggregate($count as CountryCount))
# Custom search
/Countries?$search=LaenderInEuropa
doc/Postman/OData.Sample.WebApi.Postman.json contains a ready-made collection covering select, expand, filter, count, groupby, custom search and CRUD. Import it into Postman to try them.
The client (src/Client/Program.cs) shows a menu. Press a number key to run a command:
| Key | Command | What it shows |
|---|---|---|
| 1 | Get countries | LINQ Where + OrderBy translated to $filter / $orderby |
| 2 | Get countries expanded regions | Expand, including a nested $expand |
| 3 | Get countries grouped | Grouping and counting by world region |
| 4 | Add new country | AddToCountries + SaveChanges (fake data from AutoBogus) |
| 5 | Update country AT | Load, modify, UpdateObject + SaveChanges |
| 6 | Delete co 7B6D untry in Afrika | DeleteObject + SaveChanges |
The service root is hard-coded as ServiceRoot in Program.cs. To add a command, implement IOdataCommand in src/Client/Commands/ and register it in the commands array.
The typed proxy lives in src/Client/Connected Services/ODataSampleService/. After changing the server's EDM model, run the server, then right-click the service in Visual Studio and choose Update OData Connected Service. This requires the OData Connected Service extension.
dotnet test ODataSample.slntests/OData.Sample.Tests starts the real server in memory (WebApplicationFactory) with its own temporary SQLite file and checks the seeded data (264 countries, 22 country regions, 6 world regions), $filter / $select / $orderby, nested $expand, $count, $apply, the custom $search, server paging and the CRUD operations.
The repository ships no EF migration (see Getting started), so the tests create the schema from the model before the application seeds it. The CI runs the tests on every push.
src/WebApi/appsettings.json:
{
"DbSettings": {
"ConnectionString": "Data Source=ZSQLite.db",
"DoMigrations": true,
"DoSeeding": true
}
}| Setting | Effect |
|---|---|
ConnectionString |
SQLite database file, relative to the working directory |
DoMigrations |
Apply EF Core migrations at startup |
DoSeeding |
Delete all rows and reseed from data/*.json at startup |
Set DoSeeding to false to keep changes across restarts.
- ASP.NET Core OData 9 on .NET 8
- EF Core 8 with the SQLite provider
- Microsoft.OData.Client 8 (Connected Service proxy)
- Serilog for logging
- Polly for retry policies
- Swashbuckle for Swagger
- Spectre.Console and AutoBogus in the client
Packages were updated within .NET 8 to remove known vulnerabilities (checked with dotnet list package --vulnerable --include-transitive). The unused AutoMapper packages were removed. Behaviour and endpoints are unchanged.