PanoramicData.OData.Client
10.0.123
dotnet add package PanoramicData.OData.Client --version 10.0.123
NuGet\Install-Package PanoramicData.OData.Client -Version 10.0.123
<PackageReference Include="PanoramicData.OData.Client" Version="10.0.123" />
<PackageVersion Include="PanoramicData.OData.Client" Version="10.0.123" />
<PackageReference Include="PanoramicData.OData.Client" />
paket add PanoramicData.OData.Client --version 10.0.123
#r "nuget: PanoramicData.OData.Client, 10.0.123"
#:package PanoramicData.OData.Client@10.0.123
#addin nuget:?package=PanoramicData.OData.Client&version=10.0.123
#tool nuget:?package=PanoramicData.OData.Client&version=10.0.123
PanoramicData.OData.Client
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 |
| Navigation properties | ✅ Supported | NavigateTo |
| Vendor query options | ✅ Supported | QueryOptions |
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 (throws ODataNotFoundException if not found)
var product = await client.GetByKeyAsync<Product, int>(123);
// Get by key - returns null if not found
var product = await client.GetByKeyOrDefaultAsync<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);
Navigating Related Entities (NavigateTo)
Navigate from a keyed entity to a dependent collection using NavigateTo, then use As<T>() to type the result:
// Produces: Mailboxes('user@example.com')/MailboxPermissions
var permissions = await client.For<Mailbox>()
.Key("user@example.com")
.NavigateTo(x => x.MailboxPermissions)
.As<MailboxPermission>()
.GetAsync(cancellationToken);
// Or use the typed string overload when you know the target type statically
var permissions = await client.For<Mailbox>()
.Key("user@example.com")
.NavigateTo<MailboxPermission>("MailboxPermissions")
.GetAsync(cancellationToken);
Vendor-Specific Query Options (QueryOptions)
Append raw, non-standard query parameters verbatim using QueryOptions. Values are not quoted or URL-encoded, matching Simple.OData.Client's string overload:
// Produces: ...&PropertySet=Minimum,AddressList
var propertySets = string.Join(",", new[] { "Minimum", "AddressList" });
var mailboxes = await client.For<Mailbox>()
.Select(m => new { m.UserPrincipalName, m.Alias })
.QueryOptions($"PropertySet={propertySets}")
.Filter(m => m.RecipientTypeDetails == "SharedMailbox")
.GetAsync(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 (408, 429 and 5xx)
RetryCount = 3,
RetryDelay = TimeSpan.FromSeconds(1),
// Optional: upper bound on a server-supplied Retry-After header, which is honoured in
// preference to RetryDelay. TimeSpan.Zero ignores Retry-After entirely. (default: 30 seconds)
MaximumRetryAfterDelay = TimeSpan.FromSeconds(30),
// 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");
},
// Optional: Return null instead of throwing ODataNotFoundException on 404
IgnoreResourceNotFoundException = true
});
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:
- Querying Data - Filter, select, expand, order, page, search, aggregate
- CRUD Operations - Create, read, update, delete entities
- Batch Operations - Multiple operations in single request
- Singletons - Single-instance entities like /Me
- Media & Streams - Binary data and media entities
- Entity References - Managing relationships with $ref
- Delta Queries - Change tracking and synchronization
- Service Metadata - Discovery and schema information
- Functions & Actions - Custom operations
- Async Operations - Long-running operations
- Cross-Join - Combining multiple entity sets
- Open Types - Dynamic properties
- ETag & Concurrency - Optimistic concurrency control
License
MIT License - see LICENSE file for details.
Contributing
Contributions are welcome! Please open an issue or submit a pull request.
| Product | Versions 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. |
-
net10.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
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.OData.FO
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. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.0.123 | 142 | 9/3/2026 |
| 10.0.115 | 1,172 | 8/28/2026 |
| 10.0.114 | 298 | 8/27/2026 |
| 10.0.112 | 86 | 8/27/2026 |
| 10.0.109 | 3,828 | 8/7/2026 |
| 10.0.106 | 8,883 | 7/8/2026 |
| 10.0.105 | 122 | 7/8/2026 |
| 10.0.87 | 5,806 | 6/12/2026 |
| 10.0.86 | 369 | 6/3/2026 |
| 10.0.85 | 134 | 6/3/2026 |
| 10.0.84 | 1,056 | 5/21/2026 |
| 10.0.83 | 161 | 5/19/2026 |
| 10.0.82 | 115 | 5/19/2026 |
| 10.0.81 | 115 | 5/19/2026 |
| 10.0.80 | 123 | 5/19/2026 |
| 10.0.79 | 122 | 5/19/2026 |
| 10.0.78 | 122 | 5/19/2026 |
| 10.0.75 | 114 | 5/19/2026 |
| 10.0.74 | 123 | 5/19/2026 |
| 10.0.72 | 585 | 5/18/2026 |
Security fix: credential-bearing HTTP header values are now redacted in diagnostic output.
Previously caller-supplied header values were written verbatim in two places: one Debug line
per header as it was added to a request, and the Trace request and response dumps. Any
credential attached to a query was therefore retained for the life of the consumer log store.
Note the Debug path, which fires a level higher than the Trace dumps, so a consumer running at
Debug rather than Trace was still affected.
This client applies no credential of its own, so redaction matches a general set of header
names plus name fragments rather than a scheme this library controls. The authentication
scheme and the credential length are retained, so requests remain diagnosable.
Request rendering that forms part of the wire format, such as the batch payload built by
HttpMessageContent, is deliberately unchanged. Redacting there would corrupt the request.
Consumers should upgrade and then review existing Debug and Trace logs for previously
captured credentials.
Initial release with OData V4 support, query builder, CRUD operations, pagination, retry
logic, and logging.