OData.Client

A lightweight and minimalist OData client library for .NET 9.0, designed to be simple, performant, and without heavy dependencies.

Installation

NuGet Package

dotnet add package OData.Client

Direct Project Reference

git clone https://github.com/your-username/OData.Client.git
dotnet add reference path/to/OData.Client/OData.Client.csproj

Key Features

  • Minimalist: Supports only essential operations (Select, Skip, Take, Key, Where)
  • Modern: Targeted for .NET 9.0
  • Lightweight: No dependencies on Microsoft or third-party OData libraries
  • Flexible: Fluent API for intuitive use
  • Robust: Built-in error handling, retries, and timeouts
  • Compatible: Supports different types of OData services (Standard, Minimal, Microsoft)

Quick Start

// Create a simple client
var client = OData.CreateClient("https://services.odata.org/TripPinRESTierService");

// Execute a query
var people = await client.For<Person>("People")
    .Select("UserName", "FirstName", "LastName")
    .Take(5)
    .ExecuteAsync();

Advanced Configuration

// Using the builder for advanced configuration
var client = new ODataClientBuilder()
    .WithBaseUrl("https://services.odata.org/TripPinRESTierService")
    .WithServiceCompatibility(ODataServiceType.Minimal)
    .WithTimeout(TimeSpan.FromSeconds(30))
    .WithHeader("X-Custom-Header", "CustomValue")
    .WithRetryPolicy(new ExponentialBackoffRetryPolicy(
        maxRetries: 3,
        initialDelay: TimeSpan.FromMilliseconds(200),
        maxDelay: TimeSpan.FromSeconds(2)))
    .Build();

Usage Examples

Simple Query

var client = OData.CreateClient("https://services.odata.org/TripPinRESTierService");
var people = await client.For<Person>("People")
    .Take(5)
    .ExecuteAsync();

Query with Filters

var client = OData.CreateClient("https://services.odata.org/TripPinRESTierService");
var malePeople = await client.For<Person>("People")
    .Where("Gender eq 'Male'")
    .Select("UserName", "FirstName", "LastName")
    .Skip(2)
    .Take(3)
    .ExecuteAsync();

Retrieving an Entity by Key

var client = OData.CreateClient("https://services.odata.org/TripPinRESTierService");
var person = await client.For<Person>("People")
    .Key("russellwhyte")
    .ExecuteAsync();

Error Handling

try
{
    var client = OData.CreateClient("https://services.odata.org/InvalidService");
    var people = await client.For<Person>("People").ExecuteAsync();
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"HTTP Error: {ex.Message}");
}
catch (ODataException ex)
{
    Console.WriteLine($"OData Error: {ex.Message}");
}

Using a Custom HttpClient

var httpClient = new HttpClient
{
    Timeout = TimeSpan.FromSeconds(60)
};

var customHttpClient = new DefaultHttpClient(httpClient);

var client = new ODataClient(customHttpClient, 
    new ODataClientOptions { BaseUrl = "https://services.odata.org/TripPinRESTierService" });

Authentication

Basic Authentication

var client = OData.CreateClientWithBasicAuth(
    "https://api.example.com/odata", 
    "username", 
    "password");

Bearer Token

var client = new ODataClientBuilder()
    .WithBaseUrl("https://api.example.com/odata")
    .WithBearerToken("your-token-here")
    .Build();

API Key

var client = new ODataClientBuilder()
    .WithBaseUrl("https://api.example.com/odata")
    .WithAuthentication(new ApiKeyProvider("api_key", "your-api-key-here", ApiKeyLocation.QueryString))
    .Build();

Retry Policies

Simple Retry

var client = new ODataClientBuilder()
    .WithBaseUrl("https://services.odata.org/TripPinRESTierService")
    .WithRetryPolicy(new DefaultRetryPolicy(3))
    .Build();

Exponential Backoff Retry

var client = new ODataClientBuilder()
    .WithBaseUrl("https://services.odata.org/TripPinRESTierService")
    .WithRetryPolicy(new ExponentialBackoffRetryPolicy(
        maxRetries: 5,
        initialDelay: TimeSpan.FromMilliseconds(100),
        maxDelay: TimeSpan.FromSeconds(5)))
    .Build();

Custom Retry

public class MyCustomRetryPolicy : RetryPolicyBase
{
    public MyCustomRetryPolicy() : base(3) { }

    protected override TimeSpan GetDelayBeforeNextRetry(int attemptNumber, Exception exception)
    {
        return TimeSpan.FromMilliseconds(200 * attemptNumber);
    }

    protected override bool ShouldRetry(Exception exception)
    {
        if (exception is HttpRequestException httpEx)
        {
            return httpEx.Message.Contains("500") || 
                   httpEx.Message.Contains("503");
        }
        return false;
    }
}

// Usage
var client = new ODataClientBuilder()
    .WithBaseUrl("https://services.odata.org/TripPinRESTierService")
    .WithRetryPolicy(new MyCustomRetryPolicy())
    .Build();

JSON Serializers

Newtonsoft.Json (default)

var client = new ODataClientBuilder()
    .WithBaseUrl("https://services.odata.org/TripPinRESTierService")
    .WithJsonSerializer(new NewtonsoftJsonSerializer())
    .Build();

System.Text.Json

var client = new ODataClientBuilder()
    .WithBaseUrl("https://services.odata.org/TripPinRESTierService")
    .WithJsonSerializer(new SystemTextJsonSerializer())
    .Build();

HTTP Headers

Adding Global Headers

var client = new ODataClientBuilder()
    .WithBaseUrl("https://services.odata.org/TripPinRESTierService")
    .WithHeader("X-Custom-Header", "CustomValue")
    .WithHeader("X-Api-Version", "1.0")
    .Build();

Adding Request-Specific Headers

var client = OData.CreateClient("https://services.odata.org/TripPinRESTierService");
var person = await client.For<Person>("People")
    .Key("russellwhyte")
    .WithHeader("X-Request-ID", Guid.NewGuid().ToString())
    .ExecuteAsync();

Accessing Response Headers

var client = new ODataClientBuilder()
    .WithBaseUrl("https://services.odata.org/TripPinRESTierService")
    .WithOptions(options => options.IncludeResponseHeaders = true)
    .Build();

var response = await client.For<Person>("People")
    .Key("russellwhyte")
    .ExecuteAsync();

var person = response.First();
var serverHeader = response.Headers.GetHeader("Server");

Compatibility with Different OData Services

Standard Service

var client = OData.CreateClientForService(
    "https://services.odata.org/V4/Northwind/Northwind.svc", 
    ODataServiceType.Standard);

Minimal Service (TripPin)

var client = OData.CreateClientForService(
    "https://services.odata.org/TripPinRESTierService", 
    ODataServiceType.Minimal);

Microsoft Service (Graph API)

var client = OData.CreateClientForService(
    "https://graph.microsoft.com/v1.0",
    ODataServiceType.Microsoft);

Best Practices

Client Instance Reuse

Reuse ODataClient instances to benefit from the underlying HTTP connection pooling:

// Create a single instance and reuse it
var client = OData.CreateClient("https://services.odata.org/TripPinRESTierService");

// Use the same instance for all queries
var people = await client.For<Person>("People").ExecuteAsync();
var trips = await client.For<Trip>("Trips").ExecuteAsync();

Using with Dependency Injection

// In Program.cs or Startup.cs
services.AddSingleton<IODataClient>(provider => 
{
    return OData.CreateClientForService(
        "https://services.odata.org/TripPinRESTierService",
        ODataServiceType.Minimal);
});

// In your service
public class MyService
{
    private readonly IODataClient _client;
    
    public MyService(IODataClient client)
    {
        _client = client;
    }
    
    public async Task<IEnumerable<Person>> GetPeopleAsync()
    {
        return await _client.For<Person>("People").ExecuteAsync();
    }
}

Performance Optimization

  1. Use Select to limit returned properties:

    await client.For<Person>("People")
        .Select("UserName", "FirstName", "LastName")
        .ExecuteAsync();
  2. Use Take/Skip for pagination:

    await client.For<Person>("People")
        .Skip(10)
        .Take(10)
        .ExecuteAsync();
  3. Use Where to filter results:

    await client.For<Person>("People")
        .Where("Gender eq 'Male'")
        .ExecuteAsync();
  4. Use Key to retrieve a specific entity:

    await client.For<Person>("People")
        .Key("russellwhyte")
        .ExecuteAsync();

License

MIT