OsduClient wraps all service clients with token acquisition (via the ITokenProvider you supply), data-partition-id injection, and connection management. Construct it once and use it for the lifetime of your application.
using Equinor.OsduCsharpClient.Facade;
using Equinor.OsduCsharpClient.Facade.Auth; // MsalInteractiveTokenProvider (Msal package)
using Microsoft.Extensions.Configuration;
var config = OsduConfig.FromConfiguration(builder.Configuration);
using var osdu = new OsduClient(config, new MsalInteractiveTokenProvider(config));The core package is authentication-agnostic — a token provider is required. Install the optional
Equinor.OsduCsharpClient.Msalpackage for the MSAL providers used here, or pass your ownITokenProvider. See Authentication below.
OsduConfig.FromConfiguration(IConfiguration) binds the Osdu section from any standard
.NET configuration source — appsettings.json, environment variables, user secrets, command line:
Any value can be overridden by an environment variable using the standard double-underscore
convention, e.g. Osdu__Server or Osdu__Scopes. Pass a custom section name via
OsduConfig.FromConfiguration(config, "MySection"). You can also construct OsduConfig directly:
var config = new OsduConfig
{
Server = "https://your-osdu-instance.com",
DataPartitionId = "your-partition-id",
Authority = "https://login.microsoftonline.com/<tenant-id>",
ClientId = "<client-id>",
Scopes = "api://<app-id-uri>/.default",
};
using var osdu = new OsduClient(config, new MsalInteractiveTokenProvider(config));using Equinor.OsduCsharpClient.Search.Models;
var result = await osdu.Search.Query.PostAsync(
new QueryRequest
{
Kind = new QueryRequest.QueryRequest_kind
{
QueryRequestKindString = "osdu:wks:work-product-component--WellLog:*"
},
Query = "*",
Limit = 10,
ReturnedFields = ["id", "kind", "createTime"],
});
if (result?.Results is not null)
{
foreach (var record in result.Results)
Console.WriteLine(record.AdditionalData["id"]);
}var result = await osdu.Entitlements.Groups.All.GetAsync(config =>
{
config.QueryParameters.Type = "NONE";
});
if (result?.Groups is not null)
{
foreach (var group in result.Groups)
Console.WriteLine($"{group.Name} - {group.Email}");
}OsduClient requires an ITokenProvider — the core package ships no default and depends on
no identity library. This prevents the SDK from pinning an MSAL version that could clash with
your application's, and keeps MSAL's supply-chain surface under your control rather than the
SDK's.
You have two options:
1. Bring your own. Implement the single-method ITokenProvider interface, or use the
built-in StaticTokenProvider when you already hold a token:
using Equinor.OsduCsharpClient.Facade.Auth;
public sealed class MyTokenProvider : ITokenProvider
{
public Task<string> GetTokenAsync(CancellationToken ct = default) => /* acquire token */;
}
using var osdu = new OsduClient(config, new MyTokenProvider());
// Or a pre-acquired token (ships in the core package):
using var osdu = new OsduClient(config, new StaticTokenProvider("your-bearer-token"));2. Use the optional MSAL package. Install Equinor.OsduCsharpClient.Msal for ready-made
MSAL providers with OS-encrypted token caching. Choose the mode that fits your environment:
dotnet add package Equinor.OsduCsharpClient.Msal| Provider | When to use | Config needed |
|---|---|---|
MsalInteractiveTokenProvider |
Local dev, opens browser | Authority, ClientId, Scopes |
MsalDeviceFlowTokenProvider |
Headless / SSH sessions | Authority, ClientId, Scopes |
MsalClientCredentialsTokenProvider |
CI / service-to-service | + clientSecret |
using Equinor.OsduCsharpClient.Facade.Auth;
// Interactive (opens browser on first run, then silent from cache)
using var osdu = new OsduClient(config, new MsalInteractiveTokenProvider(config));
// Device code flow (prints a URL + code to the console; no browser required on this machine)
using var osdu = new OsduClient(config, new MsalDeviceFlowTokenProvider(config));
// Client credentials (CI / service-to-service, no user interaction)
using var osdu = new OsduClient(config, new MsalClientCredentialsTokenProvider(config, clientSecret: "..."));All three MSAL providers persist the token cache to ~/.osdu/msal_cache.bin by default (override with OSDU_MSAL_CACHE_PATH env var), so silent renewal is used on subsequent runs.
OsduClient uses Microsoft.Extensions.Logging and produces no output by default. Pass your application's ILoggerFactory to enable logging:
using Microsoft.Extensions.Logging;
using var loggerFactory = LoggerFactory.Create(builder =>
builder.AddConsole().SetMinimumLevel(LogLevel.Debug));
using var osdu = new OsduClient(config, loggerFactory: loggerFactory);Two log categories are used:
| Category | What it logs | Default level to enable |
|---|---|---|
Equinor.OsduCsharpClient |
→ METHOD URL, ← STATUS URL (elapsed ms), auth flow events |
Debug |
Equinor.OsduCsharpClient.Body |
Request/response bodies (truncated to 2 KB, Authorization/Cookie headers redacted) |
Debug (opt-in) |
To enable body logging only:
// appsettings.json
{
"Logging": {
"LogLevel": {
"Equinor.OsduCsharpClient": "Debug",
"Equinor.OsduCsharpClient.Body": "Debug"
}
}
}Auth providers also accept an optional ILoggerFactory to log silent vs interactive/device/credential flow transitions.
Each service client can also be constructed directly with a Kiota IRequestAdapter when you need full control over auth and HTTP configuration.
using Microsoft.Kiota.Abstractions.Authentication;
using Microsoft.Kiota.Http.HttpClientLibrary;
var authProvider = new BaseBearerTokenAuthenticationProvider(new MyTokenProvider());
var adapter = new HttpClientRequestAdapter(authProvider)
{
BaseUrl = "https://your-osdu-instance.com/api/entitlements/v2"
};using Equinor.OsduCsharpClient.Entitlements;
var client = new EntitlementsClient(adapter);
var result = await client.Groups.All.GetAsync(config =>
{
config.QueryParameters.Type = "NONE";
config.Headers.Add("data-partition-id", "your-partition-id");
});using Equinor.OsduCsharpClient.Search;
using Equinor.OsduCsharpClient.Search.Models;
var client = new SearchClient(adapter);
var result = await client.Query.PostAsync(
new QueryRequest
{
Kind = new QueryRequest.QueryRequest_kind
{
QueryRequestKindString = "osdu:wks:work-product-component--WellLog:*"
},
Query = "*",
Limit = 10,
ReturnedFields = ["id", "kind", "createTime"],
},
config => config.Headers.Add("data-partition-id", "your-partition-id"));OSDU records share a common envelope (id, kind, acl, legal, createTime, …) but the data block varies by record kind. A WellLog, a Wellbore and a Trajectory all travel inside the same Record envelope, so no single closed C# type can describe data — the spec declares it as a free-form object.
The services with a generic Record schema — Storage, Dataset and Wellbore DDMS — expose data directly as a Kiota UntypedNode (Record.Data). The facade adds a JSON bridge in Equinor.OsduCsharpClient.Facade — ToUntypedNode() / ToJsonNode(), plus generic POCO overloads — so you can author and read data with ordinary System.Text.Json values instead of hand-building UntypedNode trees.
The example below ingests a WellLog through Wellbore DDMS; the same Record / JSON-bridge pattern applies to osdu.Storage.Records.PutAsync(...) and the Dataset registry endpoints.
Ingesting a WellLog:
using System.Text.Json.Nodes;
using Equinor.OsduCsharpClient.Facade;
using Equinor.OsduCsharpClient.WellboreDdms.Models;
var data = JsonNode.Parse("""
{
"Name": "GR Log",
"WellboreID": "partition:master-data--Wellbore:abc:",
"TopMeasuredDepth": 12345.6,
"BottomMeasuredDepth": 13856.2,
"Curves": [ { "Mnemonic": "GR", "NumberOfColumns": 1 } ]
}
""");
var record = new Record
{
Kind = "osdu:wks:work-product-component--WellLog:1.2.0",
Acl = new StorageAcl { Owners = ["..."], Viewers = ["..."] },
Legal = new Legal { /* legaltags, otherRelevantDataCountries */ },
Data = data.ToUntypedNode(),
};
var response = await osdu.WellboreDdms.Ddms.V3.Welllogs.PostAsync([record]);Reading data back:
var result = await osdu.WellboreDdms.Ddms.V3.Welllogs[welllogId].GetAsync();
JsonNode? data = result?.Data.ToJsonNode();
Console.WriteLine((string?)data?["Name"]);Bridging your own POCOs:
record.Data = myWellLog.ToUntypedNode(); // POCO -> UntypedNode
MyWellLog? wl = result?.Data.Deserialize<MyWellLog>(); // UntypedNode -> POCOJSON Merge Patch (Storage PATCH /records/{id}):
The merge-patch request body uses the same pattern — RecordMergePatchRequest.Data is also an UntypedNode. Per RFC 7396, include a JSON null to delete a nested key:
var patch = new RecordMergePatchRequest
{
Data = JsonNode.Parse("""
{
"Name": "Updated Well Name",
"RetiredField": null
}
""").ToUntypedNode(),
};
await osdu.Storage.Records[recordId].PatchAsync(patch);Some endpoints return free-form payloads that are not exposed as a typed Record — for example Search query hits, which arrive as untyped maps in AdditionalData. For those, use Kiota's NativeResponseHandler to intercept the HttpResponseMessage directly and read the raw JSON.
using Microsoft.Kiota.Abstractions;
using Equinor.OsduCsharpClient.Search.Models;
using System.Net.Http;
var nativeResponseHandler = new NativeResponseHandler();
await osdu.Search.Query.PostAsync(
new QueryRequest { Kind = new() { QueryRequestKindString = "*:*:*:*" }, Query = "*" },
config =>
{
config.Options.Add(new ResponseHandlerOption { ResponseHandler = nativeResponseHandler });
});
var httpResponse = (HttpResponseMessage)nativeResponseHandler.Value!;
var json = await httpResponse.Content.ReadAsStringAsync();
Console.WriteLine(json);Note: When a
NativeResponseHandleris used, Kiota hands off the response to the handler instead of deserializing it into a typed model. The method returns the default value (nullfor reference types). UsejsonwithSystem.Text.Jsonto access any fields you need.
using var doc = JsonDocument.Parse(json);
// Pretty-print the entire record
Console.WriteLine(JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true }));
// Extract a specific field from data
if (doc.RootElement.TryGetProperty("data", out var data)
&& data.TryGetProperty("WellboreID", out var val))
{
Console.WriteLine($"WellboreID: {val.GetString()}");
}Kiota is a typed client generator: it maps OpenAPI schemas to closed C# classes. The OSDU record data property is an open-ended object with additionalProperties: true because its shape depends on the record kind. Kiota cannot generate a meaningful type for it.
For the services with a generic Record schema (Storage, Dataset, Wellbore DDMS) the spec is patched during generation so data is emitted as a Kiota UntypedNode (Record.Data) — see generate_all.py. That makes data round-trip arbitrary JSON, and the facade's JSON bridge turns it into ergonomic System.Text.Json access for both reading and writing. For endpoints that return free-form payloads outside that Record type, NativeResponseHandler + System.Text.Json remains the way to consume free-form fields.
Well-log bulk data is served as Parquet by Wellbore DDMS — the /ddms/v3/{entity}/{id}/data endpoints negotiate application/json or application/x-parquet, and Parquet is the primary, performant format. The generated client is JSON-only (Kiota models one content type per direction — microsoft/kiota#3377), so the facade ships a hand-written bulk client at client.WellboreDdmsBulk that talks real application/x-parquet over the same authenticated transport:
// Read bulk data as Parquet (raw stream)
await using var parquet = await client.WellboreDdmsBulk.ReadParquetAsync(
recordId,
new WellboreBulkReadOptions { Curves = ["MD", "GR"], Limit = 10_000 },
cancellationToken: ct);
// Write bulk data as Parquet, creating a new record version
var version = await client.WellboreDdmsBulk.WriteParquetAsync(recordId, parquetStream, cancellationToken: ct);For large datasets (> ~10M values or > 3000 columns the server requires chunking), write via a session — one call orchestrates open → upload chunks → commit, abandoning the session automatically if any step fails:
var commit = await client.WellboreDdmsBulk.WriteParquetSessionAsync(
recordId, [chunk1, chunk2, chunk3], SessionUpdateMode.Update, cancellationToken: ct);
// Or drive the steps yourself
var session = await client.WellboreDdmsBulk.OpenSessionAsync(recordId, SessionUpdateMode.Update, cancellationToken: ct);
await client.WellboreDdmsBulk.WriteSessionChunkParquetAsync(recordId, session.Id!.Value, chunk1, cancellationToken: ct);
var result = await client.WellboreDdmsBulk.CommitSessionAsync(recordId, session.Id!.Value, cancellationToken: ct);
// ... or AbandonSessionAsync(recordId, session.Id!.Value)All methods take an entity parameter (WellLogs default, WellboreTrajectories, PpfgDataset, WellPressureTestRawMeasurement). Payloads are raw Parquet Streams — the package takes no dependency on a Parquet library; encode/decode with e.g. Parquet.Net or Apache.Arrow. Remapping to application/octet-stream is not an option: WBDDMS validates the exact media type and rejects it.
For other requests the generated clients cannot express (alternate content types, hand-built URLs), OsduClient.GetRequestAdapter(serviceAttr) returns the authenticated Kiota adapter for any registered service. Build a RequestInformation and send it directly — bearer-token auth, data-partition-id injection, and logging are all applied:
var adapter = client.GetRequestAdapter("wellbore_ddms");
var requestInfo = new RequestInformation(
Method.GET, "{+baseurl}/ddms/v3/welllogs/{record_id}/data",
new Dictionary<string, object> { { "record_id", recordId } });
requestInfo.Headers.TryAdd("Accept", "application/x-parquet");
var stream = await adapter.SendPrimitiveAsync<Stream>(requestInfo, cancellationToken: ct);