OData Integration in .NET Core Applications with One-to-Many and Many-to-One Relationships
Building Rich APIs with Real-World Data Relationships
When working with RESTful APIs in .NET Core, managing complex relationships (like one-to-many and many-to-one) can be tedious, especially if you want to support filtering, sorting, and navigation in your endpoints.
That’s where OData (Open Data Protocol) shines 🌟.
Previous version : OData Integration in .NET Core Applications
OData not only provides powerful query capabilities like $filter
, $expand
, and $orderby
, but it also makes navigating entity relationships a breeze — including one-to-many and many-to-one relationships.
In this blog, we’ll:
Briefly introduce OData
Create a real-world example using Orders and Customers
Show how to handle one-to-many and many-to-one relationships
Enable querying with
$expand
and navigation properties
Real-World Scenario: Customers and Orders
Let’s assume we’re building an e-commerce API.
One Customer can place many Orders (One-to-Many)
Each Order belongs to one Customer (Many-to-One)
Entities:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
// One-to-many
public ICollection<Order> Orders { get; set; }
}
public class Order
{
public int Id { get; set; }
public string Product { get; set; }
public decimal Amount { get; set; }
// Foreign Key
public int CustomerId { get; set; }
// Navigation
public Customer Customer { get; set; }
}
Step-by-Step Setup in .NET Core
✅ Step 1: Install Required NuGet Packages
dotnet add package Microsoft.AspNetCore.OData
✅ Step 2: Setup DbContext
👋 Become 1% better at .NET Full Stack development every day.
👆 https://dotnet-fullstack-dev.blogspot.com/
♻ Restack it Vibe matches, help others to get it
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {}
public DbSet<Customer> Customers { get; set; }
public DbSet<Order> Orders { get; set; }
}
✅ Step 3: Configure OData in Program.cs
using Microsoft.OData.ModelBuilder;
using Microsoft.AspNetCore.OData;
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseInMemoryDatabase("ODataDb"));
var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Customer>("Customers");
modelBuilder.EntitySet<Order>("Orders");
builder.Services.AddControllers().AddOData(opt =>
opt.AddRouteComponents("odata", modelBuilder.GetEdmModel())
.Filter()
.Select()
.Expand()
.OrderBy()
.SetMaxTop(100)
.Count());
var app = builder.Build();
app.UseRouting();
app.UseEndpoints(endpoints => endpoints.MapControllers());
app.Run();
✅ Step 4: Create OData Controllers
📌 CustomersController
public class CustomersController : ODataController
{
private readonly AppDbContext _context;
public CustomersController(AppDbContext context)
{
_context = context;
}
[EnableQuery]
public IQueryable<Customer> Get()
{
return _context.Customers;
}
[EnableQuery]
public SingleResult<Customer> Get([FromODataUri] int key)
{
return SingleResult.Create(_context.Customers.Where(c => c.Id == key));
}
[EnableQuery]
public IQueryable<Order> GetOrders([FromODataUri] int key)
{
return _context.Orders.Where(o => o.CustomerId == key);
}
}
📌 OrdersController
public class OrdersController : ODataController
{
private readonly AppDbContext _context;
public OrdersController(AppDbContext context)
{
_context = context;
}
[EnableQuery]
public IQueryable<Order> Get()
{
return _context.Orders;
}
[EnableQuery]
public SingleResult<Order> Get([FromODataUri] int key)
{
return SingleResult.Create(_context.Orders.Where(o => o.Id == key));
}
[EnableQuery]
public SingleResult<Customer> GetCustomer([FromODataUri] int key)
{
return SingleResult.Create(_context.Orders
.Where(o => o.Id == key)
.Select(o => o.Customer));
}
}
Using OData in Action 🧪
🔍 Get All Customers and Expand Their Orders
GET /odata/Customers?$expand=Orders
🔍 Get Orders and Expand Customer Info
GET /odata/Orders?$expand=Customer
🔍 Filter Orders by CustomerId
GET /odata/Orders?$filter=CustomerId eq 2
🔍 Get Orders for Specific Customer
GET /odata/Customers(1)/Orders
🔍 Sort Orders by Amount Descending
GET /odata/Orders?$orderby=Amount desc
Benefits of Using OData for Entity Relationships
✅ Powerful Query Support – No need to write custom endpoints for filtering, sorting, or navigation.
✅ Reduced Boilerplate – Automatic query parsing and binding.
✅ Supports Real-time Scenarios – Suitable for complex domains with nested relations.
✅ Client-Friendly – Tools like Excel, Power BI, and Swagger can auto-consume metadata.
Conclusion
OData makes dealing with one-to-many and many-to-one relationships in .NET Core APIs extremely intuitive. With just a few lines of setup, you empower your API clients to query and navigate data efficiently — without extra controller code or complex logic.
This approach is perfect for apps that deal with relational data, such as:
E-commerce (Customers → Orders → Products)
School systems (Students → Courses → Teachers)
CRM tools (Accounts → Contacts → Activities)
💡 Pro Tip: Always secure your OData endpoints using throttling, query restrictions, and authorization when exposing in production.
Want to learn more or facing issues with OData navigation? Drop your thoughts in the comments! 🚀