PanoramicData.OData.Client 10.0.64

dotnet add package PanoramicData.OData.Client --version 10.0.64
                    
NuGet\Install-Package PanoramicData.OData.Client -Version 10.0.64
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PanoramicData.OData.Client" Version="10.0.64" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PanoramicData.OData.Client" Version="10.0.64" />
                    
Directory.Packages.props
<PackageReference Include="PanoramicData.OData.Client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PanoramicData.OData.Client --version 10.0.64
                    
#r "nuget: PanoramicData.OData.Client, 10.0.64"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PanoramicData.OData.Client@10.0.64
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PanoramicData.OData.Client&version=10.0.64
                    
Install as a Cake Addin
#tool nuget:?package=PanoramicData.OData.Client&version=10.0.64
                    
Install as a Cake Tool

PanoramicData.OData.Client

Nuget Nuget License: MIT Codacy Badge

A lightweight, modern OData V4 client library for .NET 10.

What's New

See the CHANGELOG for a complete list of changes in each version.

OData V4 Feature Support

Feature Status Documentation
Querying
$filter ✅ Supported Querying
$select ✅ Supported Querying
$expand ✅ Supported Querying
$orderby ✅ Supported Querying
$top / $skip ✅ Supported Querying
$count ✅ Supported Querying
$search ✅ Supported Querying
$apply (Aggregations) ✅ Supported Querying
$compute ✅ Supported Querying
Lambda operators (any/all) ✅ Supported Querying
Type casting (derived types) ✅ Supported Querying
CRUD Operations
Create (POST) ✅ Supported CRUD
Read (GET) ✅ Supported CRUD
Update (PATCH) ✅ Supported CRUD
Replace (PUT) ✅ Supported CRUD
Delete (DELETE) ✅ Supported CRUD
Batch Operations
Batch requests ✅ Supported Batch
Changesets (atomic) ✅ Supported Batch
Singleton Entities
Get singleton ✅ Supported Singletons
Update singleton ✅ Supported Singletons
Media Entities & Streams
Get stream ($value) ✅ Supported Streams
Set stream ✅ Supported Streams
Named stream properties ✅ Supported Streams
Entity References ($ref)
Add reference ✅ Supported References
Remove reference ✅ Supported References
Set reference ✅ Supported References
Delete reference ✅ Supported References
Delta Queries
Delta tracking ✅ Supported Delta
Deleted entities ✅ Supported Delta
Delta pagination ✅ Supported Delta
Service Metadata
$metadata ✅ Supported Metadata
Service document ✅ Supported Metadata
Functions & Actions
Bound functions ✅ Supported Functions & Actions
Unbound functions ✅ Supported Functions & Actions
Bound actions ✅ Supported Functions & Actions
Unbound actions ✅ Supported Functions & Actions
Async Operations
Prefer: respond-async ✅ Supported Async
Status polling ✅ Supported Async
Advanced Features
Cross-join ($crossjoin) ✅ Supported Cross-Join
Open types ✅ Supported Open Types
ETag concurrency ✅ Supported ETag & Concurrency
Server-driven paging ✅ Supported Querying
Retry logic ✅ Supported Configuration
Custom headers ✅ Supported Querying

Installation

dotnet add package PanoramicData.OData.Client

Quick Start

using PanoramicData.OData.Client;

// Create the client
var client = new ODataClient(new ODataClientOptions
{
    BaseUrl = "https://services.odata.org/V4/OData/OData.svc/",
    ConfigureRequest = request =>
    {
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "your-token");
    }
});

// Query entities
var query = client.For<Product>("Products")
    .Filter("Price gt 100")
    .OrderBy("Name")
    .Top(10);

var response = await client.GetAsync(query);

// Get all pages automatically
var allProducts = await client.GetAllAsync(query, cancellationToken);

// Get by key
var product = await client.GetByKeyAsync<Product, int>(123);

// Create
var newProduct = await client.CreateAsync("Products", new Product { Name = "Widget" });

// Update (PATCH)
var updated = await client.UpdateAsync<Product>("Products", 123, new { Price = 150.00 });

// Delete
await client.DeleteAsync("Products", 123);

Entity Model Example

using System.Text.Json.Serialization;

public class Product
{
    [JsonPropertyName("ID")]
    public int Id { get; set; }
    
    public string Name { get; set; } = string.Empty;
    
    public string? Description { get; set; }
    
    public DateTimeOffset? ReleaseDate { get; set; }
    
    public int? Rating { get; set; }
    
    public decimal? Price { get; set; }
}

Query Builder Features

// Filtering with OData expressions
var query = client.For<Product>("Products")
    .Filter("Rating gt 3")
    .Top(3);

// Select specific fields
var query = client.For<Product>("Products")
    .Select("ID,Name,Price")
    .Top(3);

// Expand navigation properties
var query = client.For<Product>("Products")
    .Expand("Category,Supplier");

// Ordering
var query = client.For<Product>("Products")
    .OrderBy("Price desc")
    .Top(5);

// Paging
var query = client.For<Product>("Products")
    .Skip(20)
    .Top(10)
    .Count();

// Search
var query = client.For<Product>("Products")
    .Search("widget");

// Custom headers per query
var query = client.For<Product>("Products")
    .WithHeader("Prefer", "return=representation");

// Combine multiple options
var query = client.For<Product>("Products")
    .Filter("Rating gt 3")
    .Select("ID,Name,Price")
    .OrderBy("Price desc")
    .Top(10);

Fluent Query Execution

Execute queries directly from the query builder without needing to pass the query to a separate method:

// Get all matching entities
var products = await client.For<Product>("Products")
    .Filter("Price gt 100")
    .OrderBy("Name")
    .GetAsync(cancellationToken);

// Get all pages automatically
var allProducts = await client.For<Product>("Products")
    .Filter("Rating gt 3")
    .GetAllAsync(cancellationToken);

// Get first or default
var cheapest = await client.For<Product>("Products")
    .OrderBy("Price")
    .GetFirstOrDefaultAsync(cancellationToken);

// Get single entity (throws if not exactly one)
var unique = await client.For<Product>("Products")
    .Filter("Name eq 'SpecialWidget'")
    .GetSingleAsync(cancellationToken);

// Get single or default (returns null if none, throws if multiple)
var maybeOne = await client.For<Product>("Products")
    .Filter("ID eq 123")
    .GetSingleOrDefaultAsync(cancellationToken);

// Get count
var count = await client.For<Product>("Products")
    .Filter("Price gt 50")
    .GetCountAsync(cancellationToken);

Raw OData Queries

// Use raw filter strings for complex scenarios
var query = client.For<Product>("Products")
    .Filter("contains(tolower(Name), 'widget')");

// Get raw JSON response
var json = await client.GetRawAsync("Products?$filter=Price gt 100");

OData Functions and Actions

// Call a function
var query = client.For<Product>("Products")
    .Function("Microsoft.Dynamics.CRM.SearchProducts", new { SearchTerm = "widget" });
var result = await client.CallFunctionAsync<Product, List<Product>>(query);

// Call an action
var response = await client.CallActionAsync<OrderResult>(
    "Orders(123)/Microsoft.Dynamics.CRM.Ship",
    new { TrackingNumber = "ABC123" });

Logging with Dependency Injection

The client supports ILogger for detailed request/response logging:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using PanoramicData.OData.Client;

// Set up dependency injection with logging
var services = new ServiceCollection();

services.AddLogging(builder =>
{
    builder
        .SetMinimumLevel(LogLevel.Debug)
        .AddSimpleConsole(options =>
        {
            options.IncludeScopes = true;
            options.SingleLine = false;
            options.TimestampFormat = "HH:mm:ss.fff ";
        });
});

var serviceProvider = services.BuildServiceProvider();
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();

// Create the ODataClient with logging enabled
var logger = loggerFactory.CreateLogger<ODataClient>();
var client = new ODataClient(new ODataClientOptions
{
    BaseUrl = "https://services.odata.org/V4/OData.svc/",
    Logger = logger,
    RetryCount = 3,
    RetryDelay = TimeSpan.FromMilliseconds(500)
});

// Now all requests will be logged with full details
var query = client.For<Product>("Products").Top(5);
var response = await client.GetAsync(query);

Logging Levels

Level Information Logged
Trace Full HTTP traffic: request URL, method, all headers, request body, response status, response headers, response body
Debug Request URLs, methods, status codes, content lengths, parsed item counts
Warning Retry attempts for failed requests
Error Failed requests with response body

Full HTTP Traffic Logging (Trace Level)

To see complete request and response details including headers and body content, set the minimum log level to Trace:

services.AddLogging(builder =>
{
    builder
        .SetMinimumLevel(LogLevel.Trace)  // Enable full HTTP traffic logging
        .AddSimpleConsole();
});

Sample Trace output:

=== HTTP Request ===
GET https://api.example.com/Products?$top=5
--- Request Headers ---
Authorization: Bearer eyJ...
Accept: application/json
--- Request Body ---
(none for GET requests)

=== HTTP Response ===
Status: 200 OK
--- Response Headers ---
Content-Type: application/json; odata.metadata=minimal
OData-Version: 4.0
--- Response Body ---
{"@odata.context":"...","value":[{"ID":1,"Name":"Widget",...}]}

Sample Debug Log Output

12:34:56.789 dbug: PanoramicData.OData.Client.ODataClient[0]
      GetAsync<Product> - URL: Products?$top=5
12:34:56.890 dbug: PanoramicData.OData.Client.ODataClient[0]
      CreateRequest - GET Products?$top=5
12:34:57.123 dbug: PanoramicData.OData.Client.ODataClient[0]
      SendWithRetryAsync - Received OK from Products?$top=5
12:34:57.145 dbug: PanoramicData.OData.Client.ODataClient[0]
      GetAsync<Product> - Response received, content length: 1234
12:34:57.156 dbug: PanoramicData.OData.Client.ODataClient[0]
      GetAsync<Product> - Parsed 5 items from 'value' array

Configuration Options

var client = new ODataClient(new ODataClientOptions
{
    // Required: Base URL of the OData service
    BaseUrl = "https://api.example.com/odata",
    
    // Optional: Request timeout (default: 5 minutes)
    Timeout = TimeSpan.FromMinutes(5),
    
    // Optional: Retry configuration for transient failures
    RetryCount = 3,
    RetryDelay = TimeSpan.FromSeconds(1),
    
    // Optional: Provide your own HttpClient
    HttpClient = existingHttpClient,
    
    // Optional: ILogger for debug logging
    Logger = loggerInstance,
    
    // Optional: Custom JSON serialization settings
    JsonSerializerOptions = customOptions,
    
    // Optional: Configure headers for every request
    ConfigureRequest = request =>
    {
        request.Headers.Add("Custom-Header", "value");
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "token");
    }
});

Exception Handling

try
{
    var product = await client.GetByKeyAsync<Product, int>(999);
}
catch (ODataNotFoundException ex)
{
    // 404 - Entity not found
    Console.WriteLine($"Not found: {ex.RequestUrl}");
}
catch (ODataUnauthorizedException ex)
{
    // 401 - Unauthorized
    Console.WriteLine($"Unauthorized: {ex.ResponseBody}");
}
catch (ODataForbiddenException ex)
{
    // 403 - Forbidden
    Console.WriteLine($"Forbidden: {ex.ResponseBody}");
}
catch (ODataConcurrencyException ex)
{
    // 412 - ETag mismatch
    Console.WriteLine($"Concurrency conflict: {ex.RequestETag} vs {ex.CurrentETag}");
}
catch (ODataClientException ex)
{
    // Other errors
    Console.WriteLine($"Status: {ex.StatusCode}, Body: {ex.ResponseBody}");
}

Testing

The library can be tested against the public OData sample services:

// Read-only sample service
const string ODataV4ReadOnlyUri = "https://services.odata.org/V4/OData/OData.svc/";

// Read-write sample service (creates unique session)
const string ODataV4ReadWriteUri = "https://services.odata.org/V4/OData/%28S%28readwrite%29%29/OData.svc/";

// Northwind sample service
const string NorthwindV4ReadOnlyUri = "https://services.odata.org/V4/Northwind/Northwind.svc/";

// TripPin sample service
const string TripPinV4ReadWriteUri = "https://services.odata.org/V4/TripPinServiceRW/";

Documentation

For detailed documentation on each feature, see the Documentation folder:

License

MIT License - see LICENSE file for details.

Contributing

Contributions are welcome! Please open an issue or submit a pull request.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (6)

Showing the top 5 NuGet packages that depend on PanoramicData.OData.Client:

Package Downloads
MagicSuite.Api

A nuget package for the Magic Suite API

MicrosoftDynamics.Api

A .NET API for Microsoft Dynamics, based on PanoramicData OData Client

IntegratoR.OData

IntegratoR — .NET integration framework for D365 F&O using Clean Architecture, CQRS, FluentResults, and Azure Durable Functions.

IntegratoR.Hosting

IntegratoR — .NET integration framework for D365 F&O using Clean Architecture, CQRS, FluentResults, and Azure Durable Functions.

IntegratoR.OData.FO

IntegratoR — .NET integration framework for D365 F&O using Clean Architecture, CQRS, FluentResults, and Azure Durable Functions.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.0.64 62 4/16/2026
10.0.63 104 4/12/2026
10.0.61 221 3/29/2026
10.0.59 606 3/20/2026
10.0.55 2,887 1/15/2026
10.0.53 335 1/8/2026
10.0.51 374 1/6/2026
10.0.48 107 1/6/2026
10.0.47 393 12/19/2025
10.0.44 549 12/17/2025
10.0.40-beta 278 12/17/2025
10.0.37-beta 275 12/17/2025
10.0.32-beta 296 12/17/2025
10.0.29-beta 270 12/17/2025
10.0.27-beta 275 12/16/2025
10.0.26-beta 298 12/16/2025
10.0.24-beta 268 12/16/2025
10.0.21-beta 285 12/16/2025
10.0.17-beta 283 12/16/2025
10.0.15-beta 288 12/16/2025
Loading failed

Initial release with OData V4 support, query builder, CRUD operations, pagination, retry logic, and logging.