Skip to content

Latest commit

History

136 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

PanoramicData.OData.Client

NugetNugetLicense: MITCodacy 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

FeatureStatusDocumentation
Querying
$filter✅ SupportedQuerying
$select✅ SupportedQuerying
$expand✅ SupportedQuerying
$orderby✅ SupportedQuerying
$top / $skip✅ SupportedQuerying
$count✅ SupportedQuerying
$search✅ SupportedQuerying
$apply (Aggregations)✅ SupportedQuerying
$compute✅ SupportedQuerying
Lambda operators (any/all)✅ SupportedQuerying
Type casting (derived types)✅ SupportedQuerying
CRUD Operations
Create (POST)✅ SupportedCRUD
Read (GET)✅ SupportedCRUD
Update (PATCH)✅ SupportedCRUD
Replace (PUT)✅ SupportedCRUD
Delete (DELETE)✅ SupportedCRUD
Batch Operations
Batch requests✅ SupportedBatch
Changesets (atomic)✅ SupportedBatch
Singleton Entities
Get singleton✅ SupportedSingletons
Update singleton✅ SupportedSingletons
Media Entities & Streams
Get stream ($value)✅ SupportedStreams
Set stream✅ SupportedStreams
Named stream properties✅ SupportedStreams
Entity References ($ref)
Add reference✅ SupportedReferences
Remove reference✅ SupportedReferences
Set reference✅ SupportedReferences
Delete reference✅ SupportedReferences
Delta Queries
Delta tracking✅ SupportedDelta
Deleted entities✅ SupportedDelta
Delta pagination✅ SupportedDelta
Service Metadata
$metadata✅ SupportedMetadata
Service document✅ SupportedMetadata
Functions & Actions
Bound functions✅ SupportedFunctions & Actions
Unbound functions✅ SupportedFunctions & Actions
Bound actions✅ SupportedFunctions & Actions
Unbound actions✅ SupportedFunctions & Actions
Async Operations
Prefer: respond-async✅ SupportedAsync
Status polling✅ SupportedAsync
Advanced Features
Cross-join ($crossjoin)✅ SupportedCross-Join
Open types✅ SupportedOpen Types
ETag concurrency✅ SupportedETag & Concurrency
Server-driven paging✅ SupportedQuerying
Retry logic✅ SupportedConfiguration
Custom headers✅ SupportedQuerying
Navigation properties✅ SupportedNavigateTo
Vendor query options✅ SupportedQueryOptions

Installation

dotnet add package PanoramicData.OData.Client

Quick Start

usingPanoramicData.OData.Client;// Create the clientvarclient=newODataClient(newODataClientOptions{BaseUrl="https://services.odata.org/V4/OData/OData.svc/",ConfigureRequest= request =>{request.Headers.Authorization=newAuthenticationHeaderValue("Bearer","your-token");}});// Query entitiesvarquery=client.For<Product>("Products").Filter("Price gt 100").OrderBy("Name").Top(10);varresponse=awaitclient.GetAsync(query);// Get all pages automaticallyvarallProducts=awaitclient.GetAllAsync(query,cancellationToken);// Get by key (throws ODataNotFoundException if not found)varproduct=awaitclient.GetByKeyAsync<Product,int>(123);// Get by key - returns null if not foundvarproduct=awaitclient.GetByKeyOrDefaultAsync<Product,int>(123);// CreatevarnewProduct=awaitclient.CreateAsync("Products",newProduct{Name="Widget"});// Update (PATCH)varupdated=awaitclient.UpdateAsync<Product>("Products",123,new{Price=150.00});// Deleteawaitclient.DeleteAsync("Products",123);

Entity Model Example

usingSystem.Text.Json.Serialization;publicclassProduct{[JsonPropertyName("ID")]publicintId{get;set;}publicstringName{get;set;}=string.Empty;publicstring?Description{get;set;}publicDateTimeOffset?ReleaseDate{get;set;}publicint?Rating{get;set;}publicdecimal?Price{get;set;}}

Query Builder Features

// Filtering with OData expressionsvarquery=client.For<Product>("Products").Filter("Rating gt 3").Top(3);// Select specific fieldsvarquery=client.For<Product>("Products").Select("ID,Name,Price").Top(3);// Expand navigation propertiesvarquery=client.For<Product>("Products").Expand("Category,Supplier");// Orderingvarquery=client.For<Product>("Products").OrderBy("Price desc").Top(5);// Pagingvarquery=client.For<Product>("Products").Skip(20).Top(10).Count();// Searchvarquery=client.For<Product>("Products").Search("widget");// Custom headers per queryvarquery=client.For<Product>("Products").WithHeader("Prefer","return=representation");// Combine multiple optionsvarquery=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 entitiesvarproducts=awaitclient.For<Product>("Products").Filter("Price gt 100").OrderBy("Name").GetAsync(cancellationToken);// Get all pages automaticallyvarallProducts=awaitclient.For<Product>("Products").Filter("Rating gt 3").GetAllAsync(cancellationToken);// Get first or defaultvarcheapest=awaitclient.For<Product>("Products").OrderBy("Price").GetFirstOrDefaultAsync(cancellationToken);// Get single entity (throws if not exactly one)varunique=awaitclient.For<Product>("Products").Filter("Name eq 'SpecialWidget'").GetSingleAsync(cancellationToken);// Get single or default (returns null if none, throws if multiple)varmaybeOne=awaitclient.For<Product>("Products").Filter("ID eq 123").GetSingleOrDefaultAsync(cancellationToken);// Get countvarcount=awaitclient.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')/MailboxPermissionsvarpermissions=awaitclient.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 staticallyvarpermissions=awaitclient.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,AddressListvarpropertySets=string.Join(",",new[]{"Minimum","AddressList"});varmailboxes=awaitclient.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 scenariosvarquery=client.For<Product>("Products").Filter("contains(tolower(Name), 'widget')");// Get raw JSON responsevarjson=awaitclient.GetRawAsync("Products?$filter=Price gt 100");

OData Functions and Actions

// Call a functionvarquery=client.For<Product>("Products").Function("Microsoft.Dynamics.CRM.SearchProducts",new{SearchTerm="widget"});varresult=awaitclient.CallFunctionAsync<Product,List<Product>>(query);// Call an actionvarresponse=awaitclient.CallActionAsync<OrderResult>("Orders(123)/Microsoft.Dynamics.CRM.Ship",new{TrackingNumber="ABC123"});

Logging with Dependency Injection

The client supports ILogger for detailed request/response logging:

usingMicrosoft.Extensions.DependencyInjection;usingMicrosoft.Extensions.Logging;usingPanoramicData.OData.Client;// Set up dependency injection with loggingvarservices=newServiceCollection();services.AddLogging(builder =>{builder.SetMinimumLevel(LogLevel.Debug).AddSimpleConsole(options =>{options.IncludeScopes=true;options.SingleLine=false;options.TimestampFormat="HH:mm:ss.fff ";});});varserviceProvider=services.BuildServiceProvider();varloggerFactory=serviceProvider.GetRequiredService<ILoggerFactory>();// Create the ODataClient with logging enabledvarlogger=loggerFactory.CreateLogger<ODataClient>();varclient=newODataClient(newODataClientOptions{BaseUrl="https://services.odata.org/V4/OData.svc/",Logger=logger,RetryCount=3,RetryDelay=TimeSpan.FromMilliseconds(500)});// Now all requests will be logged with full detailsvarquery=client.For<Product>("Products").Top(5);varresponse=awaitclient.GetAsync(query);

Logging Levels

LevelInformation Logged
TraceFull HTTP traffic: request URL, method, all headers, request body, response status, response headers, response body
DebugRequest URLs, methods, status codes, content lengths, parsed item counts
WarningRetry attempts for failed requests
ErrorFailed 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

varclient=newODataClient(newODataClientOptions{// Required: Base URL of the OData serviceBaseUrl="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 HttpClientHttpClient=existingHttpClient,// Optional: ILogger for debug loggingLogger=loggerInstance,// Optional: Custom JSON serialization settingsJsonSerializerOptions=customOptions,// Optional: Configure headers for every requestConfigureRequest= request =>{request.Headers.Add("Custom-Header","value");request.Headers.Authorization=newAuthenticationHeaderValue("Bearer","token");},// Optional: Return null instead of throwing ODataNotFoundException on 404IgnoreResourceNotFoundException=true});

Exception Handling

try{varproduct=awaitclient.GetByKeyAsync<Product,int>(999);}catch(ODataNotFoundExceptionex){// 404 - Entity not foundConsole.WriteLine($"Not found: {ex.RequestUrl}");}catch(ODataUnauthorizedExceptionex){// 401 - UnauthorizedConsole.WriteLine($"Unauthorized: {ex.ResponseBody}");}catch(ODataForbiddenExceptionex){// 403 - ForbiddenConsole.WriteLine($"Forbidden: {ex.ResponseBody}");}catch(ODataConcurrencyExceptionex){// 412 - ETag mismatchConsole.WriteLine($"Concurrency conflict: {ex.RequestETag} vs {ex.CurrentETag}");}catch(ODataClientExceptionex){// Other errorsConsole.WriteLine($"Status: {ex.StatusCode}, Body: {ex.ResponseBody}");}

Testing

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

// Read-only sample serviceconststringODataV4ReadOnlyUri="https://services.odata.org/V4/OData/OData.svc/";// Read-write sample service (creates unique session)conststringODataV4ReadWriteUri="https://services.odata.org/V4/OData/%28S%28readwrite%29%29/OData.svc/";// Northwind sample serviceconststringNorthwindV4ReadOnlyUri="https://services.odata.org/V4/Northwind/Northwind.svc/";// TripPin sample serviceconststringTripPinV4ReadWriteUri="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.

About

A crazy-fast, MIT-licensed OData Client

Resources

Contributing

Security policy

Stars

16 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages