Table of Contents

Class HistoricalClient

Namespace
DatabentoDotNet.Historical
Assembly
DatabentoDotNet.Historical.dll

A client for Databento's historical HTTPS API: metadata, symbology, timeseries queries older than 24 hours, and batch jobs.

public sealed class HistoricalClient : IAsyncDisposable
Inheritance
HistoricalClient
Implements
Inherited Members

Examples

using DatabentoDotNet;
using DatabentoDotNet.Dbn;
using DatabentoDotNet.Historical;
using NodaTime;

// One long-lived client for the life of the process, not one per call: this type is safe for
// concurrent requests once configured.
await using var client = new HistoricalClient
{
    ApiKey = new ApiKey(Environment.GetEnvironmentVariable("DATABENTO_API_KEY")!),
};

var request = new GetRangeParams
{
    Dataset = "GLBX.MDP3",
    Symbols = Symbols.From("ESH4"),
    Schema = Schema.Trades,
    DateTimeRange = DateRange.OnDay(new LocalDate(2024, 1, 2)).ToDateTimeRange(),
    Limit = 10,
};

// Free, and it prices the request below rather than one assembled a second time by hand — which
// is the whole reason ToQuery() exists.
decimal cost = await client.Metadata.GetCostAsync(request.ToQuery());
if (cost > 0.01m)
{
    Console.WriteLine($"${cost} is more than this program will spend.");
    return;
}

// This one bills.
await using var reader = await client.Timeseries.GetRangeAsync(request);
await foreach (OwnedRecord record in reader.ReadRecordsAsync())
{
    if (record.TryGet(out TradeMsg trade))
    {
        Console.WriteLine($"{DbnTime.ToInstant(trade.IndexTs)} {trade.Price} x {trade.Size}");
    }
}

Remarks

Port of upstream's historical::Client (historical/client.rs) — its request, check_warnings, check_http_error, handle_response and handle_zstd_jsonl_response, which together are the whole HTTP transport every endpoint sits on. This type is that transport and nothing else: the endpoints themselves arrive with the subclient facades that group them — #36#39. A facade with no endpoints on it would be a public empty class, so none is declared here.

Thread-safe for concurrent requests once configured, and deliberately so — the opposite call from LiveClient. Everything below the public surface is one HttpClient, which is documented as safe for concurrent use, plus properties that are init-only and therefore frozen before the first request. Several requests may be in flight on one instance, and the intended use is one long-lived client for the life of a process rather than one per call. LiveClient is not thread-safe because one live connection is one conversation with a gateway and its record loop is a single reader by construction; nothing of the kind is true of independent HTTP requests, so nothing here pretends otherwise.

No builder. Upstream's ClientBuilder<AK> is generic type-state whose only purpose is to make "no API key" unrepresentable — build() exists only on ClientBuilder<ApiKey>. C# 11 required init properties do exactly that natively, checked by the compiler at every construction site. See PORTING.md §2, and LiveClient for the precedent in this repo.

await using var client = new HistoricalClient { ApiKey = new ApiKey(key) };

var datasets = await client.SendJsonAsync(
    HttpMethod.Get, "metadata.list_datasets", parameters: null, MyJson.Default.ListString, ct);

The transport is public, and that is a decision rather than an omission. SendAsync(HttpMethod, string, IEnumerable<KeyValuePair<string, string>>?, string?, CancellationToken) and the two readers are what the endpoint facades are built from, and they are also the escape hatch for an endpoint this library has not wrapped yet: the API has twenty (ROADMAP.md §5 lists them), and a caller who needs the twenty-first the week it ships should not have to wait for a release. Upstream's equivalents are pub(crate); this repo declares no InternalsVisibleTo anywhere, so "internal but tested" is not a shape available here.

Constructors

HistoricalClient()

Creates a client. Configure it through the init properties.

public HistoricalClient()

Remarks

All three fields are lazy, and not for cost. An init accessor runs after the constructor body, so ApiKey does not exist yet at the point where an eager constructor would want to build the HttpClient and its Authorization header from it. Deferring to first use is what makes required init properties and a fully configured HttpClient compatible at all. ExecutionAndPublication because this type is documented as safe for concurrent requests: two threads racing into the first request must get one client, not two, and the loser must not see a half-built one.

Fields

ApiVersion

The API version every request path is prefixed with.

public const int ApiVersion = 0

Field Value

int

JsonMediaType

The Accept every request carries unless one is given for it.

public const string JsonMediaType = "application/json"

Field Value

string

Properties

ApiKey

The API key to authenticate with. Validated when it is constructed.

public required ApiKey ApiKey { get; init; }

Property Value

ApiKey

Remarks

The type, never a string. That is what keeps the key out of a log line or an exception message structurally rather than carefully: ToString() is redacted, so formatting the object that holds it cannot leak it. The key reaches the wire in exactly one place — the Authorization header built in this file — and nowhere else, not as a query parameter and not as a form field.

BaseUrl

A base URL to send requests to instead of Gateway's, or null to use the gateway.

public Uri? BaseUrl { get; init; }

Property Value

Uri

Remarks

The advanced knob, as upstream documents with_url: it exists for a test harness or a proxy, and a caller pointing a production client somewhere other than Databento's own gateway has almost certainly made a mistake. It is how this library's own tests reach MockHistoricalGateway.

A path on this URL is preserved, which takes explicit work. new Uri(new Uri("http://host/api"), "v0/x") is http://host/v0/x — combining a relative URI with a base whose path has no trailing slash replaces that last segment rather than appending to it, so a proxy mounted at /api would silently lose its mount point. The effective base address is therefore normalised to end in / before anything is resolved against it.

Exceptions

ArgumentException

The URL is not absolute.

Batch

The batch.* endpoints — jobs that produce files rather than a stream.

public BatchClient Batch { get; }

Property Value

BatchClient

Remarks

The fourth and last facade (#39), cached the same way and for the same reason. One of its eight methods costs money and the rest are free: SubmitJobAsync(SubmitJobParams, CancellationToken) bills for the whole range at once, and listing, inspecting and downloading a job cost nothing — a job's files stay fetchable until they expire, so a download can be retried or resumed without a second charge. Price a job with GetCostAsync(MetadataQueryParams, CancellationToken) before submitting it; ToQuery() narrows the parameters for exactly that.

DisposesHandler

Whether DisposeAsync() disposes Handler. Defaults to true, as HttpClient's own parameter does.

public bool DisposesHandler { get; init; }

Property Value

bool

Remarks

Set it to false when the handler's lifetime belongs to somebody else — which is the IHttpMessageHandlerFactory case, where the factory pools handlers across clients and rotates them on its own schedule. Disposing one out from under it would break every other client sharing it. Ignored when Handler is null: a handler this client built is a handler this client disposes.

Gateway

The gateway to send requests to. Defaults to Bo1.

public HistoricalGateway Gateway { get; init; }

Property Value

HistoricalGateway

Remarks

Ignored when BaseUrl is set.

Handler

The HttpMessageHandler to send through, or null to let HttpClient build its own.

public HttpMessageHandler? Handler { get; init; }

Property Value

HttpMessageHandler

Remarks

This exists for IHttpClientFactory, and the defect it fixes is not socket exhaustion. HttpClient's own handler is a SocketsHttpHandler whose PooledConnectionLifetime defaults to infinite, so a client held as a singleton in a host that stays up for weeks keeps talking to whatever address hist.databento.com resolved to on its first request. A handler supplied here can bound that; nothing else on this type can.

A handler, not an HttpClient, and that is the whole design. Everything this client puts on a request it builds — HTTP Basic from the ApiKey, the validated User-Agent, the Accept header, the base address — is still built here and still built once. Handing over the whole client would mean either mutating an object this type does not own to attach the Authorization header, or letting the caller attach it — and then the key has two paths to the wire, which is exactly what ApiKey's redacted ToString() and the single-header rule exist to prevent.

LoggerFactory

Where to send this client's log messages, or null for none.

public ILoggerFactory? LoggerFactory { get; init; }

Property Value

ILoggerFactory

Remarks

This is how the API's X-Warning header surfaces, and it is the only route it has: the alternative — a warnings property on every response — means every one of the API's twenty endpoints (ROADMAP.md §5 lists them) returns a wrapper type instead of its payload, and every caller unwrapping, to carry a header that is almost always absent. That was rejected on cost, not on taste. See Internal/HistoricalLog.cs for the messages and their event ids.

Left null, this resolves to Instance — no logging configured means no logging done, and nothing is formatted or allocated for a caller who never asked.

Metadata

The metadata.* endpoints — discovery, and what a request would cost.

public MetadataClient Metadata { get; }

Property Value

MetadataClient

Remarks

The first of the four endpoint-group facades this client exposes; #38–#39 add the rest. Built once and cached, because this client is documented thread-safe for concurrent requests and a bare null-coalescing assignment would let two threads each build one.

Symbology

The symbology.* endpoints — what a symbol's instrument id was, and when.

public SymbologyClient Symbology { get; }

Property Value

SymbologyClient

Remarks

The second facade (#37), cached the same way and for the same reason. One endpoint, and it costs nothing to call.

Timeseries

The timeseries.* endpoints — the market data itself.

public TimeseriesClient Timeseries { get; }

Property Value

TimeseriesClient

Remarks

The third facade (#38), cached the same way and for the same reason. The only one whose endpoints cost money: everything on Metadata and Symbology is discovery or a billing enquiry. Price a download with GetCostAsync(MetadataQueryParams, CancellationToken) before making it.

UpgradePolicy

How to handle DBN data from an older version than this library decodes natively. Defaults to UpgradeToV3, as upstream's builder does.

public VersionUpgradePolicy UpgradePolicy { get; init; }

Property Value

VersionUpgradePolicy

Remarks

Carried here because it is a property of the client rather than of a call — upstream keeps it on Client and reads it from timeseries.get_range and the batch endpoints — but nothing in this transport consults it. It is the DBN decoder's input, and the first request whose body is DBN rather than JSON arrives with #38.

UserAgentExtension

Text to append to this library's User-Agent, identifying the application built on it, or null to send the library's own user agent alone.

public string? UserAgentExtension { get; init; }

Property Value

string

Remarks

Port of upstream's user_agent_ext (client.rs:384-387), which composes it the same way: the library's user agent, a space, then this. The composed header goes through Add(string, string) — the validating overload — so an extension that is not a well-formed sequence of user-agent products and comments is rejected when the first request is sent rather than silently reaching Databento's logs malformed.

Methods

DisposeAsync()

Releases the underlying HttpClient.

public ValueTask DisposeAsync()

Returns

ValueTask

A completed task; there is no asynchronous work to do.

Remarks

Idempotent, and safe on a client that never sent a request: the HttpClient is built on first use, so there is nothing to release until one has been. Using the client after this throws ObjectDisposedException rather than quietly building a second one.

GetPathAsync(string, IEnumerable<KeyValuePair<string, string>>?, CancellationToken)

Sends one GET to an arbitrary path on the configured host, carrying whatever request headers are given, and returns the response with its headers read and its body still on the socket.

public Task<HttpResponseMessage> GetPathAsync(string path, IEnumerable<KeyValuePair<string, string>>? headers = null, CancellationToken cancellationToken = default)

Parameters

path string

The path to fetch, resolved against the configured base URL. An absolute path — one beginning with /, which is what AbsolutePath gives — replaces the base URL's path entirely, which is what makes this a faithful port of Url::join.

headers IEnumerable<KeyValuePair<string, string>>

Request headers to add, or null for none.

cancellationToken CancellationToken

Cancels the request.

Returns

Task<HttpResponseMessage>

The response, headers read and body not yet buffered. The caller disposes it.

Remarks

Port of upstream's get_with_path (client.rs:128-137), which exists for exactly one caller in either library: a batch file's download URL is given by the API rather than composed from a slug, so it cannot go through SendAsync(HttpMethod, string, IEnumerable<KeyValuePair<string, string>>?, string?, CancellationToken).

Only the path is used, and the host the API named is discarded — deliberately, and upstream does the same. batch.list_files returns URLs on api.databento.com while the API itself is reached at hist.databento.com; upstream's base_url.join(path) keeps the configured scheme and authority and replaces only the path, and #39 measured both hosts serving byte-identical responses for the same path. Two things follow, and both are the reason to keep it rather than to "fix" it.

First, the API key never reaches a host the caller did not configure. The credential travels on this request as it does on every other one, so following a server-supplied absolute URL would be handing it to whatever host that URL named. Second, a test harness pointed at by BaseUrl keeps working: the download goes to the same loopback server as everything else, which is what lets this library's resumable-download tests run against MockHistoricalGateway at all.

headers is what SendAsync(HttpMethod, string, IEnumerable<KeyValuePair<string, string>>?, string?, CancellationToken) has no equivalent of, and it exists for Range. Values go through TryAddWithoutValidation(string, string) — the non-validating overload — because a Range is a request header whose validated form Headers models as a typed collection rather than as a string, and round-tripping through that type to send back the value already in hand would buy nothing.

Exceptions

ArgumentException

path is null or empty.

DatabentoApiException

The API answered with a non-success status.

ObjectDisposedException

The client has been disposed.

PathFor(string)

The path a slug is served at, relative to the base URL: v0/{slug}.

public static string PathFor(string slug)

Parameters

slug string

The API slug — metadata.list_datasets, timeseries.get_range. Slashes are allowed, so a batch file's path — batch/download/{user}/{job}/{file} — is a slug like any other.

Returns

string

The relative path.

Remarks

Relative, and with no leading slash, because that is the only form that composes: new Uri(baseAddress, PathFor(slug)) appends to the base address' path, where a leading slash would resolve against the authority and discard it.

MockHistoricalGateway.PathFor in this repo's test project returns the absolute path, with its leading slash, because its job is to match a recorded RecordedRequest.Path. Two different jobs that happen to share a name and a version segment; neither calls the other, deliberately. The harness is written from the API's documented behaviour rather than from this library, and a harness that computed the path the same way the client does could not catch the client computing it wrongly.

slug is interpolated, not escaped, and that is the caller's constraint to honour. Upstream's base_url.join(&format!("v{API_VERSION}/{slug}")) does not escape either, so this is faithful — but faithful is not the same as safe, and a slug is a path here rather than a value. A ? in one starts a query string and a # starts a fragment, either of which silently truncates the path instead of producing a rejected request. Endpoint slugs are literals in this library's own source and cannot contain one; a batch file's path (#39) is server-supplied and is the one place a caller passes something it did not write, so percent-encode there rather than widening this.

Exceptions

ArgumentException

slug is null or empty.

ReadJsonAsync<T>(HttpResponseMessage, JsonTypeInfo<T>, CancellationToken)

Reads response's body as one JSON document.

public static Task<T> ReadJsonAsync<T>(HttpResponseMessage response, JsonTypeInfo<T> typeInfo, CancellationToken cancellationToken = default)

Parameters

response HttpResponseMessage

The response to read.

typeInfo JsonTypeInfo<T>

The source-generated metadata for T.

cancellationToken CancellationToken

Cancels the read.

Returns

Task<T>

The deserialized body.

Type Parameters

T

The type to deserialize into.

Remarks

Port of the tail of upstream's handle_response (client.rs:207-209). It does not dispose response; SendJsonAsync<T>(HttpMethod, string, IEnumerable<KeyValuePair<string, string>>?, JsonTypeInfo<T>, CancellationToken) is the composed form that does.

A JsonTypeInfo<T> rather than a plain T, and that signature is not negotiable here. This assembly is trim- and AOT-analysed with warnings as errors, so the reflection-based JsonSerializer overloads do not merely allocate a metadata cache at run time — they fail the build (IL2026/IL3050). Each endpoint therefore supplies its own [JsonSerializable] context and passes the generated type info in, which is also what lets a consumer publish this library AOT-compiled at all.

static, because it needs nothing from the client — the response is the caller's and the decode is pure. CA1822 is what raised the question and this repo treats warnings as errors, but the answer would be the same without it: the alternative was to give the method a use for instance state it does not have, and inventing a disposed-client guard for a read that touches no client state would make await using-scoping a client and then finishing a response you already hold throw for no reason.

A decode failure here is thrown and not logged, deliberately. Upstream logs one (deserialize_json, client.rs:231-236) and this port does not; the rule that decides which of upstream's tracing sites are ported is on Internal/HistoricalLog.cs's type remarks, and this is the case it rules out. The short version: the JsonException reaches the caller carrying Path, LineNumber and BytePositionInLine, so a log line would duplicate what they already hold.

Exceptions

ArgumentNullException

An argument is null.

JsonException

The body is not valid JSON, or is the literal null.

ReadZstdJsonLinesAsync<T>(HttpResponseMessage, JsonTypeInfo<T>, CancellationToken)

Reads response's body as a zstd frame containing one JSON document per line.

public static Task<IReadOnlyList<T>> ReadZstdJsonLinesAsync<T>(HttpResponseMessage response, JsonTypeInfo<T> typeInfo, CancellationToken cancellationToken = default)

Parameters

response HttpResponseMessage

The response to read.

typeInfo JsonTypeInfo<T>

The source-generated metadata for T.

cancellationToken CancellationToken

Cancels the read.

Returns

Task<IReadOnlyList<T>>

One element per non-blank line, in the order they arrived.

Type Parameters

T

The type each line deserializes into.

Remarks

Port of upstream's handle_zstd_jsonl_response (client.rs:212-229). The frame is in the body rather than announced in Content-Encoding, which is why this decompresses the stream itself instead of leaving it to HttpClient's automatic decompression — there is no Content-Encoding for that to act on. A line that is empty or entirely whitespace is skipped, which is wider than the trailing newline a line-oriented writer leaves behind and is deliberately so: a lone \r surviving a CRLF split, or a line of spaces, is no more a JSON document than an empty one and would otherwise fail the whole read.

No historical endpoint calls this, and that is not an oversight. Upstream's handle_zstd_jsonl_response has zero call sites in src/historical/; all four are in src/reference/security.rs:49 and :76, corporate.rs:58, adjustment.rs:50 — which is M4, the reference-data milestone. It lives here because upstream defines it here, so a reader comparing the two files finds it where they expect, and because the harness already serves exactly this shape, so it could be tested against an oracle written before it existed.

Takes a JsonTypeInfo<T> for the reason given on ReadJsonAsync<T>(HttpResponseMessage, JsonTypeInfo<T>, CancellationToken): the reflection-based overloads fail this assembly's build. It is static for the reason given there too.

This is the buffering half of a pair. ReadZstdJsonLinesStreamAsync<T>(HttpResponseMessage, JsonTypeInfo<T>, CancellationToken) is the streaming half: it yields each row as it decompresses instead of collecting them, and it is the one to reach for when the response is larger than the working set or the caller wants the first row before the last one has arrived. This one is what a caller who wants the whole list — to sort it, to count it, to index into it — should keep using.

Exceptions

ArgumentNullException

An argument is null.

JsonException

A line is not valid JSON, or is the literal null.

ReadZstdJsonLinesStreamAsync<T>(HttpResponseMessage, JsonTypeInfo<T>, CancellationToken)

Reads response's body as a zstd frame containing one JSON document per line, yielding each row as it decompresses rather than collecting them all first.

public static IAsyncEnumerable<T> ReadZstdJsonLinesStreamAsync<T>(HttpResponseMessage response, JsonTypeInfo<T> typeInfo, CancellationToken cancellationToken = default)

Parameters

response HttpResponseMessage

The response to read.

typeInfo JsonTypeInfo<T>

The source-generated metadata for T.

cancellationToken CancellationToken

Cancels the enumeration.

Returns

IAsyncEnumerable<T>

One element per non-blank line, in the order they arrived.

Type Parameters

T

The type each line deserializes into.

Remarks

The streaming half of a pair. ReadZstdJsonLinesAsync<T>(HttpResponseMessage, JsonTypeInfo<T>, CancellationToken) is the buffering half: it returns an IReadOnlyList<T> once the whole body has been read, and it stays the non-streaming path for a caller who wants the complete list — to sort it, to count it, to index into it. This one holds one row at a time, so a response larger than the working set costs the same as a small one and the first row is available before the last has left the server.

Rows come out in the order they arrived, and this method claims nothing more than that. Upstream's handle_zstd_jsonl_response (client.rs:212-229) returns a Vec<R> precisely so that its callers can sort it, and all four of them do: reference/security.rs:50-53 by index and :77 by ts_effective, corporate.rs:59-63 by index, adjustment.rs:51 by ex_date. A stream cannot be sorted — sorting is what buffering is — so this does not sort, and the documented order is the server's own. Whether that differs from upstream's order observably depends on whether the server already returns rows sorted, which is a question about the live API that no mock can answer (a double returns the lines it was handed, so it agrees with whatever we assumed) and which #57 owns. There is deliberately no sorting overload here: each reference endpoint decides for itself whether it sorts, over the buffering reader.

Blank-line tolerance and null-literal rejection are the buffering reader's, not a second reading of them. A line that is empty or entirely whitespace is skipped; a line that is the JSON literal null throws JsonException with the same message. A difference between the two paths would be a bug in one of them, so the tests pin them to each other rather than asserting each in isolation.

The argument checks run at the call, not at the first MoveNextAsync, and that costs a split. A C# iterator method runs no part of its body until it is enumerated, so an await foreach-less caller who passed null would get no exception at all — the bug would be silent rather than late. Everything above returns IAsyncEnumerable<T> from an ordinary method that validates and then hands back a private iterator, which is what JsonSerializer's own DeserializeAsyncEnumerable does and what makes this behave like ReadZstdJsonLinesAsync<T>(HttpResponseMessage, JsonTypeInfo<T>, CancellationToken) at the call site. The consequence is that [EnumeratorCancellation] sits on the private iterator instead; a caller's WithCancellation still reaches it, because this method returns that iterator's enumerable unchanged.

Disposing the enumerator disposes the decompression stream and the body stream — which is what an await foreach does on its way out, an early break included. response itself is the caller's to dispose, as it is on the buffering reader; SendZstdJsonLinesStreamAsync<T>(HttpMethod, string, IEnumerable<KeyValuePair<string, string>>?, JsonTypeInfo<T>, CancellationToken) is the composed form that owns it.

Exceptions

ArgumentNullException

An argument is null.

JsonException

A line is not valid JSON, or is the literal null.

OperationCanceledException

cancellationToken was cancelled.

SendAsync(HttpMethod, string, IEnumerable<KeyValuePair<string, string>>?, string?, CancellationToken)

Sends one request to v0/{slug} and returns the response, having already logged any server warnings it carried and thrown if the API rejected it.

public Task<HttpResponseMessage> SendAsync(HttpMethod method, string slug, IEnumerable<KeyValuePair<string, string>>? parameters, string? accept = null, CancellationToken cancellationToken = default)

Parameters

method HttpMethod

The HTTP method. POST forms its parameters; anything else queries them.

slug string

The API slug, without the version prefix.

parameters IEnumerable<KeyValuePair<string, string>>

The request parameters, or null for none.

accept string

An Accept for this request alone, overriding the client's JsonMediaType default, or null to send the default. It has exactly one caller in this library: timeseries.get_range asks for application/octet-stream and is the only request in the whole historical API whose response is not JSON — upstream says as much in its own comment at historical/timeseries.rs:141. It is set on the request rather than on the client's default headers, which every request this client will ever send shares.

cancellationToken CancellationToken

Cancels the request.

Returns

Task<HttpResponseMessage>

The response, headers read and body not yet buffered.

Remarks

The one primitive every endpoint is built from. Port of upstream's request (client.rs:144-154) together with the check_warnings then check_http_error pair that every one of its response handlers opens with (client.rs:205-206).

parameters travel by HTTP method, not by endpoint. Upstream has two families — add_to_query and add_to_form — and which one an endpoint uses is decided entirely by its method: every GET in the crate queries and every POST forms. So a POST sends an application/x-www-form-urlencoded body and anything else sends a query string. One rule, and no per-endpoint table for a future endpoint to be missing from — and no method for which the parameters would silently go nowhere.

A null or empty parameters on a POST is an empty form, not an absent body. The distinction is on the wire: an absent body carries no Content-Type, and a server that branches on application/x-www-form-urlencoded — Databento's does, and so does this repo's harness — sees a different request.

Values are percent-encoded with EscapeDataString(string), which escapes every reserved character rather than only the ones a URI parser would choke on. That matters for one parameter above all: a Symbols list renders as AAPL,MSFT, and the comma has to arrive as %2C — a comma is a sub-delimiter, and a server splitting on raw ones would see a differently shaped request rather than a rejected one.

The response is returned with its headers read and its body still on the socket (ResponseHeadersRead), and it is the caller's to dispose. That is not an optimisation: #38 streams bodies larger than memory, and buffering the whole response before returning it is exactly what that endpoint cannot afford. Establishing it here means no endpoint has to remember to.

Nothing leaks on the throwing path: the error body is read, the response disposed, and only then is the exception raised.

The HttpRequestMessage is disposed when this method returns, while the response body may still be arriving. That is safe — the request has been written in full by the time HttpClient hands back headers, and nothing about reading the response consults it — but it has one visible consequence worth knowing before investigating it: response.RequestMessage.Content is a disposed HttpContent on a response held across a long read, so a caller retrying a download must rebuild the request rather than resend that one.

cancellationToken is last, after accept. Both are optional, and the reverse order — the one this method was specified with — does not build here: CA1068 requires a CancellationToken to be the last parameter, and this repo treats warnings as errors. There is no suppression of it, and the ordering is not a matter of taste worth one: a caller who passes a token positionally into an accept slot gets a compile error rather than a request that quietly cannot be cancelled.

Exceptions

ArgumentNullException

method is null.

ArgumentException

slug is null or empty.

FormatException

accept is not a media type.

DatabentoApiException

The API answered with a non-success status.

ObjectDisposedException

The client has been disposed.

SendJsonAsync<T>(HttpMethod, string, IEnumerable<KeyValuePair<string, string>>?, JsonTypeInfo<T>, CancellationToken)

public Task<T> SendJsonAsync<T>(HttpMethod method, string slug, IEnumerable<KeyValuePair<string, string>>? parameters, JsonTypeInfo<T> typeInfo, CancellationToken cancellationToken = default)

Parameters

method HttpMethod

The HTTP method.

slug string

The API slug, without the version prefix.

parameters IEnumerable<KeyValuePair<string, string>>

The request parameters, or null for none.

typeInfo JsonTypeInfo<T>

The source-generated metadata for T.

cancellationToken CancellationToken

Cancels the request.

Returns

Task<T>

The deserialized body.

Type Parameters

T

The type to deserialize into.

Remarks

The shape almost every endpoint wants, and upstream's handle_response end to end.

Exceptions

DatabentoApiException

The API answered with a non-success status.

SendZstdJsonLinesAsync<T>(HttpMethod, string, IEnumerable<KeyValuePair<string, string>>?, JsonTypeInfo<T>, CancellationToken)

public Task<IReadOnlyList<T>> SendZstdJsonLinesAsync<T>(HttpMethod method, string slug, IEnumerable<KeyValuePair<string, string>>? parameters, JsonTypeInfo<T> typeInfo, CancellationToken cancellationToken = default)

Parameters

method HttpMethod

The HTTP method.

slug string

The API slug, without the version prefix.

parameters IEnumerable<KeyValuePair<string, string>>

The request parameters, or null for none.

typeInfo JsonTypeInfo<T>

The source-generated metadata for T.

cancellationToken CancellationToken

Cancels the request.

Returns

Task<IReadOnlyList<T>>

One element per non-blank line, in the order they arrived.

Type Parameters

T

The type each line deserializes into.

Remarks

The shape the reference-data endpoints want; see ReadZstdJsonLinesAsync<T>(HttpResponseMessage, JsonTypeInfo<T>, CancellationToken) for why nothing in M3 calls it.

The buffering half of a pair — SendZstdJsonLinesStreamAsync<T>(HttpMethod, string, IEnumerable<KeyValuePair<string, string>>?, JsonTypeInfo<T>, CancellationToken) is the streaming half, which yields rows as they decompress instead of returning a list.

Exceptions

DatabentoApiException

The API answered with a non-success status.

SendZstdJsonLinesStreamAsync<T>(HttpMethod, string, IEnumerable<KeyValuePair<string, string>>?, JsonTypeInfo<T>, CancellationToken)

Sends a request and streams its body's rows as they decompress — SendAsync(HttpMethod, string, IEnumerable<KeyValuePair<string, string>>?, string?, CancellationToken) and ReadZstdJsonLinesStreamAsync<T>(HttpResponseMessage, JsonTypeInfo<T>, CancellationToken) composed, with the response disposed when the enumeration ends.

public IAsyncEnumerable<T> SendZstdJsonLinesStreamAsync<T>(HttpMethod method, string slug, IEnumerable<KeyValuePair<string, string>>? parameters, JsonTypeInfo<T> typeInfo, CancellationToken cancellationToken = default)

Parameters

method HttpMethod

The HTTP method.

slug string

The API slug, without the version prefix.

parameters IEnumerable<KeyValuePair<string, string>>

The request parameters, or null for none.

typeInfo JsonTypeInfo<T>

The source-generated metadata for T.

cancellationToken CancellationToken

Cancels the request and the enumeration.

Returns

IAsyncEnumerable<T>

One element per non-blank line, in the order they arrived.

Type Parameters

T

The type each line deserializes into.

Remarks

The streaming half of a pair; SendZstdJsonLinesAsync<T>(HttpMethod, string, IEnumerable<KeyValuePair<string, string>>?, JsonTypeInfo<T>, CancellationToken) is the buffering half. Rows come out in the order they arrived and this method sorts nothing — see ReadZstdJsonLinesStreamAsync<T>(HttpResponseMessage, JsonTypeInfo<T>, CancellationToken), which is where that decision is argued in full.

Nothing is sent until the enumeration starts, and the response lives exactly as long as the enumerator. The request is issued from inside the iterator, so the using that owns the HttpResponseMessage is unwound by IAsyncEnumerator.DisposeAsync — which is what await foreach does on its way out, whether the loop ran to the end, hit an exception, or break'd after one row. Hoisting the send out of the iterator, into the validating method above it, would compile and pass a happy-path test and would leak the socket for every caller who stopped early, so the tests prove the close from the gateway's side rather than from ours.

Split into a validating method and a private iterator for the reason ReadZstdJsonLinesStreamAsync<T>(HttpResponseMessage, JsonTypeInfo<T>, CancellationToken) gives: a bad argument should fault at the call rather than at the first MoveNextAsync — or, for a caller who never enumerates, not at all. The checks duplicate SendAsync(HttpMethod, string, IEnumerable<KeyValuePair<string, string>>?, string?, CancellationToken)'s own deliberately, because inside an iterator its checks no longer run when the caller makes the call.

Exceptions

ArgumentNullException

An argument is null.

ArgumentException

slug is null or empty.

DatabentoApiException

The API answered with a non-success status.

OperationCanceledException

cancellationToken was cancelled.