{"id":48540203,"url":"https://github.com/panoramicdata/panoramicdata.odata.client","last_synced_at":"2026-04-08T04:01:55.858Z","repository":{"id":332497172,"uuid":"1116846461","full_name":"panoramicdata/PanoramicData.OData.Client","owner":"panoramicdata","description":"A crazy-fast, MIT-licensed OData Client","archived":false,"fork":false,"pushed_at":"2026-03-20T02:16:38.000Z","size":346,"stargazers_count":9,"open_issues_count":5,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-27T08:24:24.298Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/panoramicdata.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-15T13:15:56.000Z","updated_at":"2026-03-22T16:51:15.000Z","dependencies_parsed_at":"2026-01-14T09:01:52.360Z","dependency_job_id":null,"html_url":"https://github.com/panoramicdata/PanoramicData.OData.Client","commit_stats":null,"previous_names":["panoramicdata/panoramicdata.odata.client"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/panoramicdata/PanoramicData.OData.Client","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/panoramicdata%2FPanoramicData.OData.Client","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/panoramicdata%2FPanoramicData.OData.Client/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/panoramicdata%2FPanoramicData.OData.Client/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/panoramicdata%2FPanoramicData.OData.Client/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/panoramicdata","download_url":"https://codeload.github.com/panoramicdata/PanoramicData.OData.Client/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/panoramicdata%2FPanoramicData.OData.Client/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31539229,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-07T16:28:08.000Z","status":"online","status_checked_at":"2026-04-08T02:00:06.127Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-08T04:01:54.847Z","updated_at":"2026-04-08T04:01:55.849Z","avatar_url":"https://github.com/panoramicdata.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"﻿# PanoramicData.OData.Client\n\n[![Nuget](https://img.shields.io/nuget/v/PanoramicData.OData.Client)](https://www.nuget.org/packages/PanoramicData.OData.Client/)\n[![Nuget](https://img.shields.io/nuget/dt/PanoramicData.OData.Client)](https://www.nuget.org/packages/PanoramicData.OData.Client/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Codacy Badge](https://app.codacy.com/project/badge/Grade/ee4c431534a0455baf5733c36aa87a45)](https://app.codacy.com/gh/panoramicdata/PanoramicData.OData.Client/dashboard?utm_source=gh\u0026utm_medium=referral\u0026utm_content=\u0026utm_campaign=Badge_grade)\n\nA lightweight, modern OData V4 client library for .NET 10.\n\n## What's New\n\nSee the [CHANGELOG](CHANGELOG.md) for a complete list of changes in each version.\n\n## OData V4 Feature Support\n\n| Feature | Status | Documentation |\n|---------|--------|---------------|\n| **Querying** | | |\n| $filter | ✅ Supported | [Querying](Documentation/querying.md#filtering-filter) |\n| $select | ✅ Supported | [Querying](Documentation/querying.md#selecting-fields-select) |\n| $expand | ✅ Supported | [Querying](Documentation/querying.md#expanding-related-entities-expand) |\n| $orderby | ✅ Supported | [Querying](Documentation/querying.md#ordering-results-orderby) |\n| $top / $skip | ✅ Supported | [Querying](Documentation/querying.md#paging-skip-top) |\n| $count | ✅ Supported | [Querying](Documentation/querying.md#counting-results-count) |\n| $search | ✅ Supported | [Querying](Documentation/querying.md#searching-search) |\n| $apply (Aggregations) | ✅ Supported | [Querying](Documentation/querying.md#aggregations-apply) |\n| $compute | ✅ Supported | [Querying](Documentation/querying.md#computed-properties-compute) |\n| Lambda operators (any/all) | ✅ Supported | [Querying](Documentation/querying.md#filtering-filter) |\n| Type casting (derived types) | ✅ Supported | [Querying](Documentation/querying.md#derived-types-type-casting) |\n| **CRUD Operations** | | |\n| Create (POST) | ✅ Supported | [CRUD](Documentation/crud.md#creating-entities) |\n| Read (GET) | ✅ Supported | [CRUD](Documentation/crud.md#reading-entities) |\n| Update (PATCH) | ✅ Supported | [CRUD](Documentation/crud.md#updating-entities-patch) |\n| Replace (PUT) | ✅ Supported | [CRUD](Documentation/crud.md#replacing-entities-put) |\n| Delete (DELETE) | ✅ Supported | [CRUD](Documentation/crud.md#deleting-entities) |\n| **Batch Operations** | | |\n| Batch requests | ✅ Supported | [Batch](Documentation/batch.md#creating-a-batch) |\n| Changesets (atomic) | ✅ Supported | [Batch](Documentation/batch.md#changesets-atomic-transactions) |\n| **Singleton Entities** | | |\n| Get singleton | ✅ Supported | [Singletons](Documentation/singletons.md#getting-a-singleton) |\n| Update singleton | ✅ Supported | [Singletons](Documentation/singletons.md#updating-a-singleton) |\n| **Media Entities \u0026 Streams** | | |\n| Get stream ($value) | ✅ Supported | [Streams](Documentation/streams.md#media-entities) |\n| Set stream | ✅ Supported | [Streams](Documentation/streams.md#setting-stream-content) |\n| Named stream properties | ✅ Supported | [Streams](Documentation/streams.md#named-stream-properties) |\n| **Entity References ($ref)** | | |\n| Add reference | ✅ Supported | [References](Documentation/references.md#adding-references-collection) |\n| Remove reference | ✅ Supported | [References](Documentation/references.md#removing-references-collection) |\n| Set reference | ✅ Supported | [References](Documentation/references.md#setting-references-single-valued) |\n| Delete reference | ✅ Supported | [References](Documentation/references.md#deleting-references-single-valued) |\n| **Delta Queries** | | |\n| Delta tracking | ✅ Supported | [Delta](Documentation/delta.md#overview) |\n| Deleted entities | ✅ Supported | [Delta](Documentation/delta.md#understanding-delta-responses) |\n| Delta pagination | ✅ Supported | [Delta](Documentation/delta.md#getting-changes) |\n| **Service Metadata** | | |\n| $metadata | ✅ Supported | [Metadata](Documentation/metadata.md#retrieving-metadata) |\n| Service document | ✅ Supported | [Metadata](Documentation/metadata.md#service-document) |\n| **Functions \u0026 Actions** | | |\n| Bound functions | ✅ Supported | [Functions \u0026 Actions](Documentation/functions-actions.md#bound-functions-entity-set) |\n| Unbound functions | ✅ Supported | [Functions \u0026 Actions](Documentation/functions-actions.md#unbound-functions) |\n| Bound actions | ✅ Supported | [Functions \u0026 Actions](Documentation/functions-actions.md#calling-actions) |\n| Unbound actions | ✅ Supported | [Functions \u0026 Actions](Documentation/functions-actions.md#unbound-action) |\n| **Async Operations** | | |\n| Prefer: respond-async | ✅ Supported | [Async](Documentation/async-operations.md#async-action-calls) |\n| Status polling | ✅ Supported | [Async](Documentation/async-operations.md#polling-for-completion) |\n| **Advanced Features** | | |\n| Cross-join ($crossjoin) | ✅ Supported | [Cross-Join](Documentation/cross-join.md#overview) |\n| Open types | ✅ Supported | [Open Types](Documentation/open-types.md#overview) |\n| ETag concurrency | ✅ Supported | [ETag \u0026 Concurrency](Documentation/etag-concurrency.md#overview) |\n| Server-driven paging | ✅ Supported | [Querying](Documentation/querying.md#server-driven-paging) |\n| Retry logic | ✅ Supported | [Configuration](#configuration-options) |\n| Custom headers | ✅ Supported | [Querying](Documentation/querying.md#custom-headers) |\n\n## Installation\n\n```bash\ndotnet add package PanoramicData.OData.Client\n```\n\n## Quick Start\n\n```csharp\nusing PanoramicData.OData.Client;\n\n// Create the client\nvar client = new ODataClient(new ODataClientOptions\n{\n    BaseUrl = \"https://services.odata.org/V4/OData/OData.svc/\",\n    ConfigureRequest = request =\u003e\n    {\n        request.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", \"your-token\");\n    }\n});\n\n// Query entities\nvar query = client.For\u003cProduct\u003e(\"Products\")\n    .Filter(\"Price gt 100\")\n    .OrderBy(\"Name\")\n    .Top(10);\n\nvar response = await client.GetAsync(query);\n\n// Get all pages automatically\nvar allProducts = await client.GetAllAsync(query, cancellationToken);\n\n// Get by key\nvar product = await client.GetByKeyAsync\u003cProduct, int\u003e(123);\n\n// Create\nvar newProduct = await client.CreateAsync(\"Products\", new Product { Name = \"Widget\" });\n\n// Update (PATCH)\nvar updated = await client.UpdateAsync\u003cProduct\u003e(\"Products\", 123, new { Price = 150.00 });\n\n// Delete\nawait client.DeleteAsync(\"Products\", 123);\n```\n\n## Entity Model Example\n\n```csharp\nusing System.Text.Json.Serialization;\n\npublic class Product\n{\n    [JsonPropertyName(\"ID\")]\n    public int Id { get; set; }\n    \n    public string Name { get; set; } = string.Empty;\n    \n    public string? Description { get; set; }\n    \n    public DateTimeOffset? ReleaseDate { get; set; }\n    \n    public int? Rating { get; set; }\n    \n    public decimal? Price { get; set; }\n}\n```\n\n## Query Builder Features\n\n```csharp\n// Filtering with OData expressions\nvar query = client.For\u003cProduct\u003e(\"Products\")\n    .Filter(\"Rating gt 3\")\n    .Top(3);\n\n// Select specific fields\nvar query = client.For\u003cProduct\u003e(\"Products\")\n    .Select(\"ID,Name,Price\")\n    .Top(3);\n\n// Expand navigation properties\nvar query = client.For\u003cProduct\u003e(\"Products\")\n    .Expand(\"Category,Supplier\");\n\n// Ordering\nvar query = client.For\u003cProduct\u003e(\"Products\")\n    .OrderBy(\"Price desc\")\n    .Top(5);\n\n// Paging\nvar query = client.For\u003cProduct\u003e(\"Products\")\n    .Skip(20)\n    .Top(10)\n    .Count();\n\n// Search\nvar query = client.For\u003cProduct\u003e(\"Products\")\n    .Search(\"widget\");\n\n// Custom headers per query\nvar query = client.For\u003cProduct\u003e(\"Products\")\n    .WithHeader(\"Prefer\", \"return=representation\");\n\n// Combine multiple options\nvar query = client.For\u003cProduct\u003e(\"Products\")\n    .Filter(\"Rating gt 3\")\n    .Select(\"ID,Name,Price\")\n    .OrderBy(\"Price desc\")\n    .Top(10);\n```\n\n## Fluent Query Execution\n\nExecute queries directly from the query builder without needing to pass the query to a separate method:\n\n```csharp\n// Get all matching entities\nvar products = await client.For\u003cProduct\u003e(\"Products\")\n    .Filter(\"Price gt 100\")\n    .OrderBy(\"Name\")\n    .GetAsync(cancellationToken);\n\n// Get all pages automatically\nvar allProducts = await client.For\u003cProduct\u003e(\"Products\")\n    .Filter(\"Rating gt 3\")\n    .GetAllAsync(cancellationToken);\n\n// Get first or default\nvar cheapest = await client.For\u003cProduct\u003e(\"Products\")\n    .OrderBy(\"Price\")\n    .GetFirstOrDefaultAsync(cancellationToken);\n\n// Get single entity (throws if not exactly one)\nvar unique = await client.For\u003cProduct\u003e(\"Products\")\n    .Filter(\"Name eq 'SpecialWidget'\")\n    .GetSingleAsync(cancellationToken);\n\n// Get single or default (returns null if none, throws if multiple)\nvar maybeOne = await client.For\u003cProduct\u003e(\"Products\")\n    .Filter(\"ID eq 123\")\n    .GetSingleOrDefaultAsync(cancellationToken);\n\n// Get count\nvar count = await client.For\u003cProduct\u003e(\"Products\")\n    .Filter(\"Price gt 50\")\n    .GetCountAsync(cancellationToken);\n```\n\n## Raw OData Queries\n\n```csharp\n// Use raw filter strings for complex scenarios\nvar query = client.For\u003cProduct\u003e(\"Products\")\n    .Filter(\"contains(tolower(Name), 'widget')\");\n\n// Get raw JSON response\nvar json = await client.GetRawAsync(\"Products?$filter=Price gt 100\");\n```\n\n## OData Functions and Actions\n\n```csharp\n// Call a function\nvar query = client.For\u003cProduct\u003e(\"Products\")\n    .Function(\"Microsoft.Dynamics.CRM.SearchProducts\", new { SearchTerm = \"widget\" });\nvar result = await client.CallFunctionAsync\u003cProduct, List\u003cProduct\u003e\u003e(query);\n\n// Call an action\nvar response = await client.CallActionAsync\u003cOrderResult\u003e(\n    \"Orders(123)/Microsoft.Dynamics.CRM.Ship\",\n    new { TrackingNumber = \"ABC123\" });\n```\n\n## Logging with Dependency Injection\n\nThe client supports `ILogger` for detailed request/response logging:\n\n```csharp\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing PanoramicData.OData.Client;\n\n// Set up dependency injection with logging\nvar services = new ServiceCollection();\n\nservices.AddLogging(builder =\u003e\n{\n    builder\n        .SetMinimumLevel(LogLevel.Debug)\n        .AddSimpleConsole(options =\u003e\n        {\n            options.IncludeScopes = true;\n            options.SingleLine = false;\n            options.TimestampFormat = \"HH:mm:ss.fff \";\n        });\n});\n\nvar serviceProvider = services.BuildServiceProvider();\nvar loggerFactory = serviceProvider.GetRequiredService\u003cILoggerFactory\u003e();\n\n// Create the ODataClient with logging enabled\nvar logger = loggerFactory.CreateLogger\u003cODataClient\u003e();\nvar client = new ODataClient(new ODataClientOptions\n{\n    BaseUrl = \"https://services.odata.org/V4/OData.svc/\",\n    Logger = logger,\n    RetryCount = 3,\n    RetryDelay = TimeSpan.FromMilliseconds(500)\n});\n\n// Now all requests will be logged with full details\nvar query = client.For\u003cProduct\u003e(\"Products\").Top(5);\nvar response = await client.GetAsync(query);\n```\n\n### Logging Levels\n\n| Level | Information Logged |\n|-------|-------------------|\n| `Trace` | **Full HTTP traffic**: request URL, method, all headers, request body, response status, response headers, response body |\n| `Debug` | Request URLs, methods, status codes, content lengths, parsed item counts |\n| `Warning` | Retry attempts for failed requests |\n| `Error` | Failed requests with response body |\n\n### Full HTTP Traffic Logging (Trace Level)\n\nTo see complete request and response details including headers and body content, set the minimum log level to `Trace`:\n\n```csharp\nservices.AddLogging(builder =\u003e\n{\n    builder\n        .SetMinimumLevel(LogLevel.Trace)  // Enable full HTTP traffic logging\n        .AddSimpleConsole();\n});\n```\n\nSample Trace output:\n```\n=== HTTP Request ===\nGET https://api.example.com/Products?$top=5\n--- Request Headers ---\nAuthorization: Bearer eyJ...\nAccept: application/json\n--- Request Body ---\n(none for GET requests)\n\n=== HTTP Response ===\nStatus: 200 OK\n--- Response Headers ---\nContent-Type: application/json; odata.metadata=minimal\nOData-Version: 4.0\n--- Response Body ---\n{\"@odata.context\":\"...\",\"value\":[{\"ID\":1,\"Name\":\"Widget\",...}]}\n```\n\n### Sample Debug Log Output\n\n```\n12:34:56.789 dbug: PanoramicData.OData.Client.ODataClient[0]\n      GetAsync\u003cProduct\u003e - URL: Products?$top=5\n12:34:56.890 dbug: PanoramicData.OData.Client.ODataClient[0]\n      CreateRequest - GET Products?$top=5\n12:34:57.123 dbug: PanoramicData.OData.Client.ODataClient[0]\n      SendWithRetryAsync - Received OK from Products?$top=5\n12:34:57.145 dbug: PanoramicData.OData.Client.ODataClient[0]\n      GetAsync\u003cProduct\u003e - Response received, content length: 1234\n12:34:57.156 dbug: PanoramicData.OData.Client.ODataClient[0]\n      GetAsync\u003cProduct\u003e - Parsed 5 items from 'value' array\n```\n\n## Configuration Options\n\n```csharp\nvar client = new ODataClient(new ODataClientOptions\n{\n    // Required: Base URL of the OData service\n    BaseUrl = \"https://api.example.com/odata\",\n    \n    // Optional: Request timeout (default: 5 minutes)\n    Timeout = TimeSpan.FromMinutes(5),\n    \n    // Optional: Retry configuration for transient failures\n    RetryCount = 3,\n    RetryDelay = TimeSpan.FromSeconds(1),\n    \n    // Optional: Provide your own HttpClient\n    HttpClient = existingHttpClient,\n    \n    // Optional: ILogger for debug logging\n    Logger = loggerInstance,\n    \n    // Optional: Custom JSON serialization settings\n    JsonSerializerOptions = customOptions,\n    \n    // Optional: Configure headers for every request\n    ConfigureRequest = request =\u003e\n    {\n        request.Headers.Add(\"Custom-Header\", \"value\");\n        request.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", \"token\");\n    }\n});\n```\n\n## Exception Handling\n\n```csharp\ntry\n{\n    var product = await client.GetByKeyAsync\u003cProduct, int\u003e(999);\n}\ncatch (ODataNotFoundException ex)\n{\n    // 404 - Entity not found\n    Console.WriteLine($\"Not found: {ex.RequestUrl}\");\n}\ncatch (ODataUnauthorizedException ex)\n{\n    // 401 - Unauthorized\n    Console.WriteLine($\"Unauthorized: {ex.ResponseBody}\");\n}\ncatch (ODataForbiddenException ex)\n{\n    // 403 - Forbidden\n    Console.WriteLine($\"Forbidden: {ex.ResponseBody}\");\n}\ncatch (ODataConcurrencyException ex)\n{\n    // 412 - ETag mismatch\n    Console.WriteLine($\"Concurrency conflict: {ex.RequestETag} vs {ex.CurrentETag}\");\n}\ncatch (ODataClientException ex)\n{\n    // Other errors\n    Console.WriteLine($\"Status: {ex.StatusCode}, Body: {ex.ResponseBody}\");\n}\n```\n\n## Testing\n\nThe library can be tested against the public OData sample services:\n\n```csharp\n// Read-only sample service\nconst string ODataV4ReadOnlyUri = \"https://services.odata.org/V4/OData/OData.svc/\";\n\n// Read-write sample service (creates unique session)\nconst string ODataV4ReadWriteUri = \"https://services.odata.org/V4/OData/%28S%28readwrite%29%29/OData.svc/\";\n\n// Northwind sample service\nconst string NorthwindV4ReadOnlyUri = \"https://services.odata.org/V4/Northwind/Northwind.svc/\";\n\n// TripPin sample service\nconst string TripPinV4ReadWriteUri = \"https://services.odata.org/V4/TripPinServiceRW/\";\n```\n\n## Documentation\n\nFor detailed documentation on each feature, see the Documentation folder:\n\n- [Querying Data](Documentation/querying.md) - Filter, select, expand, order, page, search, aggregate\n- [CRUD Operations](Documentation/crud.md) - Create, read, update, delete entities\n- [Batch Operations](Documentation/batch.md) - Multiple operations in single request\n- [Singletons](Documentation/singletons.md) - Single-instance entities like /Me\n- [Media \u0026 Streams](Documentation/streams.md) - Binary data and media entities\n- [Entity References](Documentation/references.md) - Managing relationships with $ref\n- [Delta Queries](Documentation/delta.md) - Change tracking and synchronization\n- [Service Metadata](Documentation/metadata.md) - Discovery and schema information\n- [Functions \u0026 Actions](Documentation/functions-actions.md) - Custom operations\n- [Async Operations](Documentation/async-operations.md) - Long-running operations\n- [Cross-Join](Documentation/cross-join.md) - Combining multiple entity sets\n- [Open Types](Documentation/open-types.md) - Dynamic properties\n- [ETag \u0026 Concurrency](Documentation/etag-concurrency.md) - Optimistic concurrency control\n\n## License\n\nMIT License - see LICENSE file for details.\n\n## Contributing\n\nContributions are welcome! Please open an issue or submit a pull request.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpanoramicdata%2Fpanoramicdata.odata.client","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpanoramicdata%2Fpanoramicdata.odata.client","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpanoramicdata%2Fpanoramicdata.odata.client/lists"}