Thanks to visit codestin.com
Credit goes to github.com

Skip to content

A modern, type-safe .NET library for building OData queries using LINQ

Notifications You must be signed in to change notification settings

joadan/Linq2OData

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

181 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Warning

Under development! API is frequently updated, documentation may not be correct.

Linq2OData

.NET NuGet

A modern, type-safe .NET library for building OData queries using LINQ expressions. Supports OData v2, v3, and v4 with automatic syntax adaptation.

✨ Features

  • Type-Safe LINQ Queries - Write strongly-typed C# expressions, not error-prone strings
  • Auto Client Generation - Generate typed client from OData $metadata
  • Smart Projections - Use .Select() to retrieve only the fields you need with deep nesting support
  • Multi-Version Support - Works with OData v2, v3, and v4 seamlessly
  • Full OData Support - $filter, $expand, $orderby, $top, $skip, $count, $select

📦 Installation

dotnet add package Linq2OData.Core
dotnet add package Linq2OData.Generator

🚀 Quick Start

1. Generate Client from Metadata

The typical workflow is to generate a type-safe client from your OData service's $metadata.

Option A: Use the Web Generator

Visit https://joadan.github.io/Linq2OData/generate-client to:

  • Upload your $metadata XML file(s)
  • Configure client name and namespace
  • Download generated client as a ZIP file

Option B: Programmatic Generation

using Linq2OData.Generator;
using Linq2OData.Generator.Models;

// Fetch metadata from your OData service
var httpClient = new HttpClient();
var metadataXml = await httpClient.GetStringAsync("https://your-api.com/odata/$metadata");

// Configure and generate client
var request = new ClientRequest
{
    Name = "MyODataClient",
    Namespace = "MyApp.OData"
};
request.AddMetadata(metadataXml);

var generator = new ClientGenerator(request);
generator.GenerateClient(outputFolder: "./Generated");

What Gets Generated:

  • Client class with typed query builders (MyODataClient.cs)
  • Entity types in Types/ folder - all your data models
  • Input types in Inputs/ folder - for create/update operations
  • Enums - any enumeration types from metadata

2. Use the Generated Client

// Initialize with HttpClient
var httpClient = new HttpClient 
{ 
    BaseAddress = new Uri("https://your-api.com/odata/")
};
var client = new MyODataClient(httpClient);

// Query with type-safe LINQ expressions
var products = await client
    .Query<Product>()
    .Filter(p => p.Price > 100 && p.InStock)
    .Order(p => p.Name)
    .Top(20)
    .ExecuteAsync();

// Complex queries with expands
var suppliers = await client
    .Query<Supplier>()
    .Expand(s => s.Products)
    .Filter(s => s.Country == "USA")
    .ExecuteAsync();

🎯 Common Operations

Filtering

// Comparisons and logical operators
.Filter(p => p.Price > 10 && p.Price < 100)

// String operations  
.Filter(p => p.Name.Contains("Phone") || p.Name.StartsWith("Apple"))

// Navigation properties
.Filter(p => p.Category.Name == "Electronics")

// Date comparisons
.Filter(p => p.CreatedDate > DateTime.Now.AddMonths(-6))

Expanding Related Data

// Expand single navigation property
.Expand(s => s.Address)

// Expand collection navigation property
.Expand(s => s.Orders)

// Nested expand - chain properties directly
.Expand(s => s.Customer.Address)
.Expand(s => s.Customer.Address.Country)

// Multiple root-level expands
.Expand(s => s.Products)
.Expand(s => s.Address)
.Expand(s => s.Orders)

Note: Each .Expand() call adds another navigation property to expand. For nested navigation, chain properties using dot notation (e.g., s.Customer.Address.Country).

OData Version Differences (handled automatically):

  • v4: $expand=Customer($expand=Address($expand=Country))
  • v2/v3: $expand=Customer/Address/Country

Projections (Select Specific Fields)

Use .Select() to retrieve only the fields you need, reducing payload size and improving performance. This generates the OData $select and $expand query parameters.

Important

Server vs Client Evaluation: Only $select and $expand are sent to the OData server. Any filtering, ordering, or computed properties within the .Select() expression are evaluated client-side after the data is retrieved.

Basic Examples

// Simple property selection
var products = await client
    .Query<Product>()
    .Select(list => list.Select(p => new { p.Name, p.Price }))
    .ExecuteAsync();
// Generates: $select=Name,Price

// Include navigation properties
var products = await client
    .Query<Product>()
    .Select(list => list.Select(p => new 
    { 
        p.Name, 
        p.Price,
        CategoryName = p.Category.Name  // Nested property access
    }))
    .ExecuteAsync();
// Generates: $select=Name,Price&$expand=Category($select=Name)

// Single entity retrieval (different syntax)
var person = await client
    .Get<Person>(p => p.ID = 5)
    .Select(p => new { p.Name, p.Email })  // Note: operates on single entity
    .ExecuteAsync();
// Generates: Persons(ID=5)?$select=Name,Email

Note: .Query() uses .Select(list => list.Select(p => ...)) (operates on list), while .Get() uses .Select(p => ...) (operates on single entity).

Projecting to Custom Types

// Define a DTO
public class ProductDto
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string CategoryName { get; set; }
    public decimal DiscountPrice { get; set; }
}

// Project to custom type with server projection + client computation
var products = await client
    .Query<Product>()
    .Filter(p => p.Price > 100)
    .Select(list => list.Select(p => new ProductDto
    {
        Name = p.Name,                           // ✅ Server sends
        Price = p.Price,                         // ✅ Server sends
        CategoryName = p.Category.Name,          // ✅ Server sends via $expand
        DiscountPrice = p.Price * 0.9m           // ⚠️ Computed client-side
    }))
    .ExecuteAsync();
// Server sends: Name, Price, Category.Name (minimal data)
// Client computes: DiscountPrice

Benefits:

  • 🚀 Performance - Retrieve only needed data
  • 📦 Smaller Payloads - Less bandwidth usage
  • 🎯 Type-Safe - Compile-time checking
  • 🔄 Version Agnostic - Handles OData v2/v3/v4 automatically

Ordering & Pagination

// Order by multiple fields
.OrderBy(p => p.Category.Name)
    .ThenByDescending(p => p.Rating)
    .ThenBy(p => p.Price)

// Pagination
.Skip(20)
.Top(10)

// Get total count
.Count(true)

CRUD Operations

All types and inputs are generated automatically:

// Create (using generated input type)
var input = new ProductInput 
{ 
    Name = "New Product", 
    Price = 99.99m 
};
var created = await client
    .Create<Product>()
    .WithInput(input)
    .ExecuteAsync();

// Update
await client
    .Update<Product>(p => p.ID = 123)
    .WithInput(input)
    .ExecuteAsync();

// Delete
await client
    .Delete<Product>(p => p.ID = 123)
    .ExecuteAsync();

// Get single entity
var product = await client
    .Get<Product>(p => p.ID = 123)
    .Expand(p => p.Category)
    .ExecuteAsync();

🔄 OData Version Support

The library automatically adapts to your OData version. Key differences handled:

Feature OData v2/v3 OData v4
Nested expand Products/Category Products($expand=Category)
Date format /Date(1234567890000)/ ISO 8601
Collections Wrapped in "results" Direct arrays

All handled transparently by the library.

📖 Documentation

For detailed documentation and examples:

🛠️ Advanced Usage

Manual Client Setup (without generator)

If you need to use the library without code generation:

// Define entity manually
[ODataEntitySet("Products")]
public class Product : IODataEntitySet
{
    public int ID { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string __Key => $"ID={ID}";
}

// Use ODataClient directly
var client = new ODataClient(httpClient, ODataVersion.V4);
var result = await client.QueryEntitySetAsync<Product>(...);

Custom JSON Serialization

var client = new MyODataClient(httpClient);
client.ODataClient.JsonOptions.PropertyNameCaseInsensitive = true;

🤝 Contributing

Contributions welcome! See GitHub Issues for open items.

git clone https://github.com/joadan/Linq2OData.git
dotnet build
dotnet test

📄 License

Copyright 2026 (c) Joakim Dangården. All rights reserved.


DocumentationNuGetIssues

About

A modern, type-safe .NET library for building OData queries using LINQ

Topics

Resources

Stars

Watchers

Forks

Contributors 2

  •  
  •  

Languages