Table of Contents

WebRateProvider Class

Definition

Namespace
Bodu.Financial.ExchangeRates
Assembly
Bodu.Financial.ExchangeRates.dll
Package
Bodu.Financial.ExchangeRates 1.0.0
Source
WebRateProvider.cs

Provides the shared machinery for an exchange-rate provider that materializes a remote feed into an in-memory RateBook snapshot: it owns the accumulator and the immutable snapshot, implements the full synchronous and asynchronous lookup matrix once, and optionally owns the HttpClient used to reach the feed. Derived types supply only the feed-specific fetch.

public abstract class WebRateProvider : IDatedRateProvider, IRateProvider, IPairRateLoader, IHistoricalRateProvider, IDisposable
Inheritance
WebRateProvider
Implements
Derived
Inherited Members
Extension Methods

Remarks

HttpClient ownership. A derived provider constructed without a caller-supplied client builds and owns its own (typically through Create(string?, TimeSpan, long)), passing it to this base so it is disposed with the provider. A provider constructed with a caller-supplied client passes null as the owned client, leaving its lifetime - and its HTTP contract (user agent, timeout) - to the caller. This is the path a dependency-injection registration uses, supplying a client from IHttpClientFactory.

Lookup matrix. All getters resolve against the current immutable snapshot. The synchronous point and range getters block to fetch on a miss only when AllowSynchronousNetworkAccess is enabled; the asynchronous getters always await a coverage-aware fetch. The undated getters resolve the most recent rate as of the current instant supplied by the time provider. Derived types implement the feed-specific EnsureLoadedAsync(CurrencyPair, DateOnly, DateOnly, CancellationToken) and IsLoaded(CurrencyPair, DateOnly, DateOnly) and accumulate fetched observations through AddObservations(IEnumerable<ExchangeRate>, DateTimeOffset?) followed by RebuildSnapshot(), all under SyncRoot.

using Bodu.Financial;

// A derived provider supplies only the feed-specific fetch and coverage check.
sealed class MyFeedProvider : WebRateProvider
{
    public MyFeedProvider(HttpClient client) : base(client, timeProvider: null) { }

    protected override string ProviderId => "MyFeed";
    protected override bool AllowSynchronousNetworkAccess => false;
    protected override TimeSpan DefaultLookback => TimeSpan.FromDays(7);

    protected override bool IsLoaded(CurrencyPair pair, DateOnly startDate, DateOnly endDate) =>
        false;   // real implementations track covered windows

    protected override async ValueTask EnsureLoadedAsync(
        CurrencyPair pair, DateOnly startDate, DateOnly endDate, CancellationToken cancellationToken)
    {
        IEnumerable<ExchangeRate> fetched = await FetchFromFeedAsync(pair, startDate, endDate, cancellationToken);
        lock (SyncRoot)
        {
            AddObservations(fetched, DateTimeOffset.UtcNow);
            RebuildSnapshot();
        }
    }
}

Constructors

WebRateProvider(HttpClient?, TimeProvider?)

Initializes a new instance of the WebRateProvider class.

protected WebRateProvider(HttpClient? ownedHttpClient, TimeProvider? timeProvider)

Parameters

ownedHttpClient HttpClient

The HTTP client this provider should own and dispose, or null when the client is caller-supplied and its lifetime is the caller's responsibility.

timeProvider TimeProvider

The time source used to resolve the current instant for the undated surfaces. null selects System.

Properties

AllowSynchronousNetworkAccess

Gets a value indicating whether a synchronous lookup may block to fetch a missing window on demand.

protected abstract bool AllowSynchronousNetworkAccess { get; }

Property Value

bool

true when synchronous getters may block on the network; otherwise false.

Remarks

When enabled, the synchronous getters block on the async fetch, which can deadlock if invoked on a thread carrying a captured SynchronizationContext (classic ASP.NET, a WPF/WinForms UI thread). The synchronous path guards against this by throwing InvalidOperationException when Current is non-null; enable this only for code that calls the getters from a thread-pool thread (or use the asynchronous API).

DefaultLookback

Gets the look-back window used when a single-rate lookup must fetch on demand; the provider fetches the window ending on the requested date and spanning this duration.

protected abstract TimeSpan DefaultLookback { get; }

Property Value

TimeSpan

The look-back window.

HistoryAvailability

Gets the history depth this provider advertises: how far back it can serve rates.

public virtual RateHistoryAvailability HistoryAvailability { get; }

Property Value

RateHistoryAvailability

The advertised availability; the base reports Unbounded. A derived type whose feed publishes only a bounded window overrides this to declare it.

ProviderId

Gets the provider identifier stamped on every rate this provider produces.

protected abstract string ProviderId { get; }

Property Value

string

The provider identifier.

SyncRoot

Gets the synchronization object guarding the accumulator and snapshot; derived types lock on it while checking coverage and accumulating a fetch so the fetch publishes atomically.

protected object SyncRoot { get; }

Property Value

object

The synchronization object.

TimeProvider

Gets the time source used to resolve the current instant.

protected TimeProvider TimeProvider { get; }

Property Value

TimeProvider

The time provider.

Methods

AddObservations(IEnumerable<ExchangeRate>, DateTimeOffset?)

Upserts a batch of fetched observations into the accumulator under the provider's identifier, invoking OnObservationIngested(ExchangeRate) for each. The caller must hold SyncRoot and follow the batch with RebuildSnapshot().

protected int AddObservations(IEnumerable<ExchangeRate> rates, DateTimeOffset? fetchedAtUtc = null)

Parameters

rates IEnumerable<ExchangeRate>

The observations to upsert.

fetchedAtUtc DateTimeOffset?

The UTC instant at which the batch was downloaded, recorded at the series grain as load provenance, or null when not tracked.

Returns

int

The number of observations upserted.

CreateRangeInvertedException(DateOnly, DateOnly)

Creates the exception thrown when an inclusive date range is inverted. Derived types may override to throw a feed-specific exception type.

protected virtual Exception CreateRangeInvertedException(DateOnly startDate, DateOnly endDate)

Parameters

startDate DateOnly

The inclusive start of the range.

endDate DateOnly

The inclusive end of the range.

Returns

Exception

The exception to throw.

Dispose()

Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.

public void Dispose()

Dispose(bool)

Releases the resources used by this provider; disposes the owned HttpClient when one was created.

protected virtual void Dispose(bool disposing)

Parameters

disposing bool

true when called from Dispose(); false when called from a finalizer.

EnsureLoadedAsync(CurrencyPair, DateOnly, DateOnly, CancellationToken)

Ensures the inclusive window for a pair has been fetched and accumulated, idempotently. Implementations perform their own coverage check, request coalescing, fetch, and accumulation (via AddObservations(IEnumerable<ExchangeRate>, DateTimeOffset?) and RebuildSnapshot() under SyncRoot).

protected abstract ValueTask EnsureLoadedAsync(CurrencyPair pair, DateOnly startDate, DateOnly endDate, CancellationToken cancellationToken)

Parameters

pair CurrencyPair

The currency pair to ensure data for. Feeds that fetch by range, feed, or file may ignore it.

startDate DateOnly

The inclusive start of the window.

endDate DateOnly

The inclusive end of the window.

cancellationToken CancellationToken

A token to observe while awaiting the fetch.

Returns

ValueTask

A task that completes when the window has been loaded.

FormatRateNotFound(string, string, DateOnly)

Formats the message for the KeyNotFoundException thrown when a single-rate lookup fails. Derived types may override to use a feed-specific resource string.

protected virtual string FormatRateNotFound(string fromIsoCode, string toIsoCode, DateOnly date)

Parameters

fromIsoCode string

The source-currency ISO code.

toIsoCode string

The destination-currency ISO code.

date DateOnly

The requested date.

Returns

string

The exception message.

GetLoadedBook()

Returns the immutable book of every observation this provider has fetched and accumulated so far.

public RateBook GetLoadedBook()

Returns

RateBook

The current immutable RateBook; empty until the first fetch completes.

Remarks

The returned book is immutable and pinned at call time: later fetches replace the provider's internal book wholesale and never mutate an instance already handed out, so the result is safe to share across threads, to query after the provider is disposed, and to use as a deterministic offline snapshot. Call again after further loads to observe newly accumulated data; no reference identity is promised across calls.

The book is the composable export primitive: rewrap it with FixedDatedRateProvider(RateBook, IEnumerable<string>) to apply a custom provider-priority policy, or use ToBuilder() to edit a copy. For the ready-to-query equivalent see GetLoadedSnapshot().

Exceptions

ObjectDisposedException

Thrown when the provider has been disposed.

GetLoadedPairs()

Gets the distinct currency pairs for which observations have been accumulated so far.

public IReadOnlyCollection<CurrencyPair> GetLoadedPairs()

Returns

IReadOnlyCollection<CurrencyPair>

A snapshot of the loaded pairs; empty when nothing has been loaded.

GetLoadedSnapshot()

Returns an immutable, ready-to-query provider over every observation this provider has fetched and accumulated so far.

public FixedDatedRateProvider GetLoadedSnapshot()

Returns

FixedDatedRateProvider

The current immutable FixedDatedRateProvider snapshot; it resolves no rates until the first fetch completes.

Remarks

The snapshot is the instance this provider itself reads from - it is rebuilt once per fetch, so handing it out costs nothing - and it is pinned at call time: later fetches replace it wholesale and never mutate an instance already handed out. Use it for deterministic, offline, disposal-independent lookups over what has been loaded. Its Book is the same instance GetLoadedBook() returns at the same moment.

Exceptions

ObjectDisposedException

Thrown when the provider has been disposed.

GetRate(string, string, RateLookupOptions?)

Resolves the most recent available exchange rate from fromIsoCode to toIsoCode under options, throwing if no rate is available.

public RateLookupResult GetRate(string fromIsoCode, string toIsoCode, RateLookupOptions? options = null)

Parameters

fromIsoCode string

The source-currency ISO-style code.

toIsoCode string

The destination-currency ISO-style code.

options RateLookupOptions

The lookup rules to apply. null selects the implementation's default most-recent policy.

Returns

RateLookupResult

The resolved RateLookupResult.

Exceptions

ArgumentNullException

Thrown if fromIsoCode or toIsoCode is null.

ArgumentException

Thrown if either ISO code is not a three-character uppercase ASCII code, or if options is invalid.

KeyNotFoundException

Thrown if no rate is available for the request.

GetRate(string, string, DateOnly, RateLookupOptions?)

Resolves the exchange rate from fromIsoCode to toIsoCode on date under options, throwing if no rate is available.

public RateLookupResult GetRate(string fromIsoCode, string toIsoCode, DateOnly date, RateLookupOptions? options = null)

Parameters

fromIsoCode string

The source-currency ISO-style code.

toIsoCode string

The destination-currency ISO-style code.

date DateOnly

The calendar date for which a rate is required.

options RateLookupOptions

The lookup rules to apply, including date-resolution policy and tolerance. null is treated as Exact.

Returns

RateLookupResult

The resolved RateLookupResult.

Exceptions

ArgumentNullException

Thrown if fromIsoCode or toIsoCode is null.

ArgumentException

Thrown if either ISO code is not a three-character uppercase ASCII code, or if options is invalid (for example, Exact with non-zero tolerance).

ArgumentOutOfRangeException

Thrown if options contains a negative tolerance or an undefined enum value.

KeyNotFoundException

Thrown if no rate is available for the request under the supplied options.

GetRateAsync(string, string, RateLookupOptions?, CancellationToken)

Asynchronously resolves the most recent available exchange rate from fromIsoCode to toIsoCode under options, throwing if no rate is available.

public ValueTask<RateLookupResult> GetRateAsync(string fromIsoCode, string toIsoCode, RateLookupOptions? options = null, CancellationToken cancellationToken = default)

Parameters

fromIsoCode string

The source-currency ISO-style code.

toIsoCode string

The destination-currency ISO-style code.

options RateLookupOptions

The lookup rules to apply. null selects the implementation's default most-recent policy.

cancellationToken CancellationToken

A token to observe while awaiting the operation.

Returns

ValueTask<RateLookupResult>

A task that yields the resolved RateLookupResult.

Exceptions

ArgumentNullException

Thrown if fromIsoCode or toIsoCode is null.

ArgumentException

Thrown if either ISO code is not a three-character uppercase ASCII code, or if options is invalid.

KeyNotFoundException

Thrown if no rate is available for the request.

GetRateAsync(string, string, DateOnly, RateLookupOptions?, CancellationToken)

Asynchronously resolves the exchange rate from fromIsoCode to toIsoCode on date under options, throwing if no rate is available.

public ValueTask<RateLookupResult> GetRateAsync(string fromIsoCode, string toIsoCode, DateOnly date, RateLookupOptions? options = null, CancellationToken cancellationToken = default)

Parameters

fromIsoCode string

The source-currency ISO-style code.

toIsoCode string

The destination-currency ISO-style code.

date DateOnly

The calendar date for which a rate is required.

options RateLookupOptions

The lookup rules to apply, including date-resolution policy and tolerance. null is treated as Exact.

cancellationToken CancellationToken

A token to observe while awaiting the operation.

Returns

ValueTask<RateLookupResult>

A task that yields the resolved RateLookupResult.

Exceptions

ArgumentNullException

Thrown if fromIsoCode or toIsoCode is null.

ArgumentException

Thrown if either ISO code is not a three-character uppercase ASCII code, or if options is invalid.

ArgumentOutOfRangeException

Thrown if options contains a negative tolerance or an undefined enum value.

KeyNotFoundException

Thrown if no rate is available for the request under the supplied options.

GetRates(string, string, DateOnly, DateOnly)

Returns every available rate from fromIsoCode to toIsoCode whose observation date falls within the inclusive range startDate to endDate.

public RateRangeResult GetRates(string fromIsoCode, string toIsoCode, DateOnly startDate, DateOnly endDate)

Parameters

fromIsoCode string

The source-currency ISO-style code.

toIsoCode string

The destination-currency ISO-style code.

startDate DateOnly

The inclusive start of the range.

endDate DateOnly

The inclusive end of the range.

Returns

RateRangeResult

An RateRangeResult carrying the rates in the range ordered by date, the requested window, and the observed span; the result is empty when no rates are available.

Remarks

The lookup is range-based and does not apply a date-resolution policy: it returns the observations that exist within the window rather than resolving a single date. An implementation backed by a remote feed may block to fetch on demand, or serve only already-loaded data; use GetRatesAsync(string, string, DateOnly, DateOnly, CancellationToken) to fetch without blocking.

Exceptions

ArgumentNullException

Thrown if fromIsoCode or toIsoCode is null.

ArgumentException

Thrown if either ISO code is not a three-character uppercase ASCII code, or if endDate precedes startDate.

GetRatesAsync(string, string, DateOnly, DateOnly, CancellationToken)

Asynchronously returns every available rate from fromIsoCode to toIsoCode whose observation date falls within the inclusive range startDate to endDate.

public ValueTask<RateRangeResult> GetRatesAsync(string fromIsoCode, string toIsoCode, DateOnly startDate, DateOnly endDate, CancellationToken cancellationToken = default)

Parameters

fromIsoCode string

The source-currency ISO-style code.

toIsoCode string

The destination-currency ISO-style code.

startDate DateOnly

The inclusive start of the range.

endDate DateOnly

The inclusive end of the range.

cancellationToken CancellationToken

A token to observe while awaiting the operation.

Returns

ValueTask<RateRangeResult>

A task that yields an RateRangeResult carrying the rates in the range ordered by date, the requested window, and the observed span; the result is empty when no rates are available.

Remarks

The lookup is range-based and does not apply a date-resolution policy: it returns the observations that exist within the window rather than resolving a single date. Implementations backed by a remote feed may fetch on demand, which is why the method is asynchronous.

Exceptions

ArgumentNullException

Thrown if fromIsoCode or toIsoCode is null.

ArgumentException

Thrown if either ISO code is not a three-character uppercase ASCII code, or if endDate precedes startDate.

IsLoaded(CurrencyPair, DateOnly, DateOnly)

Reports whether the inclusive window for a pair has already been fetched, so the synchronous lookup path can skip a redundant blocking fetch.

protected abstract bool IsLoaded(CurrencyPair pair, DateOnly startDate, DateOnly endDate)

Parameters

pair CurrencyPair

The currency pair to test.

startDate DateOnly

The inclusive start of the window.

endDate DateOnly

The inclusive end of the window.

Returns

bool

true when the window is already covered; otherwise false.

LoadCoalescedAsync(string, Func<CancellationToken, Task>, CancellationToken)

Runs load for key, or joins the load already in flight for that key, so concurrent callers requesting the same endpoint window share a single fetch rather than each issuing a duplicate request. Derived types call this from EnsureLoadedAsync(CurrencyPair, DateOnly, DateOnly, CancellationToken) with a key identifying the unit they download - an era, a feed, a date range, a pair-and-window.

protected Task LoadCoalescedAsync(string key, Func<CancellationToken, Task> load, CancellationToken cancellationToken)

Parameters

key string

The key identifying the load; equal keys share one in-flight fetch.

load Func<CancellationToken, Task>

The fetch to run on a miss, invoked with a token decoupled from any single caller.

cancellationToken CancellationToken

A token that abandons this caller's wait on the shared fetch.

Returns

Task

A task that completes when the load for key completes.

Remarks

The shared fetch runs under None, so one caller's cancellation abandons only its own wait and never faults the fetch for the other joiners - appropriate for the idempotent cache-warming loads whose result populates the shared snapshot. The in-flight entry is released as soon as the fetch completes, including on failure, so a fault never poisons the key and the next caller starts a fresh attempt.

Exceptions

ArgumentNullException

Thrown when key or load is null.

OperationCanceledException

Thrown when cancellationToken is signalled before the shared fetch completes; the shared fetch continues to completion for any other joiner.

LoadPairAsync(string, string, DateOnly, DateOnly, CancellationToken)

Fetches and accumulates the inclusive window for a currency pair, unless that window is already covered.

public Task LoadPairAsync(string fromIsoCode, string toIsoCode, DateOnly startDate, DateOnly endDate, CancellationToken cancellationToken = default)

Parameters

fromIsoCode string

The source-currency ISO code.

toIsoCode string

The destination-currency ISO code.

startDate DateOnly

The inclusive start of the range.

endDate DateOnly

The inclusive end of the range.

cancellationToken CancellationToken

A token to observe while awaiting the fetch.

Returns

Task

A task that completes when the pair's window has been loaded.

Remarks

The load is delegated to the feed-specific EnsureLoadedAsync(CurrencyPair, DateOnly, DateOnly, CancellationToken), so it warms whatever unit the provider downloads - a single pair, an era, a feed, or a date range - to cover the requested window.

Exceptions

ArgumentNullException

Thrown when an ISO code is null.

ArgumentException

Thrown when an ISO code is malformed, the range is inverted, or the provider does not serve the requested pair.

OnObservationIngested(ExchangeRate)

Called once per observation as it is ingested, for derived-type diagnostics. The default does nothing.

protected virtual void OnObservationIngested(ExchangeRate rate)

Parameters

rate ExchangeRate

The observation being ingested.

OnSynchronousNetworkFetch(DateOnly)

Called after a synchronous lookup blocks to fetch on demand, for derived-type diagnostics. The default does nothing.

protected virtual void OnSynchronousNetworkFetch(DateOnly date)

Parameters

date DateOnly

The date around which the fetch was performed.

RebuildSnapshot()

Rebuilds the immutable book and lookup snapshot from the accumulator. The caller must hold SyncRoot.

protected void RebuildSnapshot()

TryGetRate(string, string, DateOnly, RateLookupOptions?, out RateLookupResult)

Attempts to resolve the exchange rate from fromIsoCode to toIsoCode on date under options, returning a flag indicating whether a rate was found.

public bool TryGetRate(string fromIsoCode, string toIsoCode, DateOnly date, RateLookupOptions? options, out RateLookupResult result)

Parameters

fromIsoCode string

The source-currency ISO-style code.

toIsoCode string

The destination-currency ISO-style code.

date DateOnly

The calendar date for which a rate is required.

options RateLookupOptions

The lookup rules to apply, including date-resolution policy and tolerance. null is treated as Exact.

result RateLookupResult

When this method returns true, contains the resolved RateLookupResult; otherwise, contains default.

Returns

bool

true if a rate was resolved; otherwise false.

Exceptions

ArgumentNullException

Thrown if fromIsoCode or toIsoCode is null.

ArgumentException

Thrown if either ISO code is not a three-character uppercase ASCII code, or if options is invalid.

ArgumentOutOfRangeException

Thrown if options contains a negative tolerance or an undefined enum value.

ValidateRangeRequest(string, string, DateOnly, DateOnly)

Validates a range request against feed-specific preconditions before any fetch is attempted. The default does nothing; derived types may override to reject unsupported pairs (for example, a single-issuer feed that quotes only against one base currency).

protected virtual void ValidateRangeRequest(string fromIsoCode, string toIsoCode, DateOnly startDate, DateOnly endDate)

Parameters

fromIsoCode string

The source-currency ISO code.

toIsoCode string

The destination-currency ISO code.

startDate DateOnly

The inclusive start of the range.

endDate DateOnly

The inclusive end of the range.

Explicit Interface Implementations

IRateProvider.GetRate(string, string)

Returns the exchange rate that converts one unit of fromIsoCode to units of toIsoCode.

decimal IRateProvider.GetRate(string fromIsoCode, string toIsoCode)

Parameters

fromIsoCode string

The source currency's ISO 4217 code.

toIsoCode string

The destination currency's ISO 4217 code.

Returns

decimal

The rate.

Exceptions

KeyNotFoundException

No rate is available for the requested pair.

Applies to

ProductVersions
.NET8, 10