Table of Contents

CachingRateProviderBase Class

Definition

Namespace
Bodu.Financial.ExchangeRates.Caching
Assembly
Bodu.Financial.ExchangeRates.Caching.dll
Package
Bodu.Financial.ExchangeRates.Caching 1.0.0
Source
CachingRateProviderBase.RefreshAhead.cs

Provides the shared caching mechanism for a provider that wraps a single inner IDatedRateProvider over a single-provider IRateCache: it serves fresh rates from the cache and delegates to the inner provider only on a miss, caching what the inner provider returns. Derived types supply the wrapped inner provider.

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

Remarks

The provider implements the same IDatedRateProvider contract the caller resolves, so it can be inserted transparently, and also the timeless IRateProvider surface, which resolves the current UTC date under DefaultLookupOptions. The cache's Provider identifies the source: it selects the caching duration from ProviderExpiry (falling back to DefaultExpiry) and tags both cached rows and log messages.

Single-date lookups serve per-row fresh observations and cache the resolved row on a miss. Range lookups serve from the cache only when its recorded coverage contains the whole requested window - that is, every day in the window was actually fetched and is still fresh - so an interior day that was never fetched forces a refetch rather than being served from a sparse set of rows. On a miss the whole range is refetched from the inner provider and written back through a single atomic StoreFetchedRange(CurrencyPair, IReadOnlyList<CachedRate>, DateOnly, DateOnly, TimeSpan, DateTimeOffset) that merges the rows and records the covered window together, even when the fetch returned no rows, so an empty-but-fetched window is not refetched on the next lookup. To group several sources behind one entry point, wrap each in its own caching provider and compose them with an AggregatingRateProvider.

When the inner provider advertises its history depth through IHistoricalRateProvider and RespectHistoryAvailability is enabled (the default), misses for dates the source has declared unavailable are not delegated: a single-date lookup outside the advertised history surfaces as an ordinary miss without an inner call, and a range fetch is clamped to start at the advertised earliest date - or skipped entirely when the whole window precedes it - while the full requested window is still recorded as covered, so the unavailable prefix is not re-asked until normal expiry.

Because this provider is itself an IDatedRateProvider and accepts one as its inner source, caching providers also stack: wrapping a cached provider in a second caching provider forms a tiered read-through - a fast outer cache (for example in-memory) over a durable inner one (for example SQLite) over the origin - where each layer is consulted in turn and only a miss falls through. Bind every layer's cache to the same Provider so a served rate is tagged with the correct source.

Both the single-date and range surfaces additionally emit a provenance record alongside each hit/miss diagnostic, recording whether the rate was resolved live or from the cache, the cache backend that served it, and - for a cache serve - the age of the served data. The provenance event is logged at RateProvenanceLogLevel.

Request coalescing is deliberately delegated to the inner provider rather than performed here, so the synchronous and asynchronous surfaces stay identical: both shipped origin bases already single-flight their downloads - WebRateProvider directly through its SingleFlightCoordinator, and PairWebRateProvider through the same coordinator via its per-pair-and-window coalescing bridge - so concurrent misses for the same source collapse onto one fetch at the layer where the cost actually lives, and decorator-level coalescing would be redundant.

The provider is IDisposable. By default it does not dispose the inner provider it wraps, because the inner is supplied by the caller (and, under dependency injection, owned by the container). Pass ownsInner as true at construction to make disposing this provider also dispose a disposable inner - the case where a single owner composes a self-owning source (for example a provider that builds its own HttpClient) behind the cache by hand.

Constructors

CachingRateProviderBase(IRateCache, CachingRateOptions, TimeProvider?, ILogger?, bool)

Initializes a new instance of the CachingRateProviderBase class.

protected CachingRateProviderBase(IRateCache cache, CachingRateOptions options, TimeProvider? timeProvider, ILogger? logger = null, bool ownsInner = false)

Parameters

cache IRateCache

The single-provider cache that serves fresh rates and stores resolved observations.

options CachingRateOptions

The options carrying the caching durations.

timeProvider TimeProvider

The time source used to evaluate freshness and stamp newly cached rows. null selects System.

logger ILogger

The logger that records cache hits, misses, and refetches. null selects Instance.

ownsInner bool

true to dispose a disposable inner provider when this provider is disposed; otherwise false to leave the inner's lifetime to its owner.

Exceptions

ArgumentNullException

Thrown when cache or options is null.

ArgumentException

Thrown when options fails validation.

Properties

HistoryAvailability

Gets the history depth this provider advertises, forwarded from the inner provider it wraps. The cache itself adds no history of its own - it can only hold what the inner once served - so the decorator is exactly as deep as its source.

public RateHistoryAvailability HistoryAvailability { get; }

Property Value

RateHistoryAvailability

The inner provider's advertised availability when it implements IHistoricalRateProvider; otherwise Unbounded.

Inner

Gets the inner provider consulted on a cache miss.

protected abstract IDatedRateProvider Inner { get; }

Property Value

IDatedRateProvider

The wrapped inner provider.

Methods

Dispose()

Releases the resources held by this provider, disposing the inner provider when ownsInner was set at construction and the inner is IDisposable.

public void Dispose()

Remarks

Disposal is idempotent. The cache is not disposed here: the shipped caches hold no unmanaged resources, and a caller-supplied cache is owned by its caller. Pending background refresh-ahead work is abandoned, not drained: no new refresh is scheduled after disposal, and an already scheduled refresh that observes the disposed state completes as a no-op.

GetRate(string, string)

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

public decimal 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.

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.

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.

WarmAsync(IEnumerable<(string From, string To)>, DateOnly, DateOnly, CancellationToken)

Warms the cache for a set of currency pairs over a date window, fetching each pair's window through the normal range read-through path so misses populate rows and coverage while already covered pairs cost only a cache read.

public ValueTask<int> WarmAsync(IEnumerable<(string From, string To)> pairs, DateOnly startDate, DateOnly endDate, CancellationToken cancellationToken = default)

Parameters

pairs IEnumerable<(string From, string To)>

The currency pairs to warm, each a from/to ISO-code tuple.

startDate DateOnly

The inclusive first date of the window to warm.

endDate DateOnly

The inclusive last date of the window to warm.

cancellationToken CancellationToken

Cancels the warm-up.

Returns

ValueTask<int>

The number of pairs warmed successfully.

Remarks

Up to four pairs are fetched concurrently; the shipped origin providers already single-flight their downloads, so the bounded parallelism only overlaps fetches of different pairs. A pair whose fetch fails is logged at Warning and skipped - the remaining pairs still warm - and is excluded from the returned count. Cancellation is the one failure that is not swallowed: a cancelled cancellationToken aborts the warm-up and propagates.

Each pair goes through GetRatesAsync(string, string, DateOnly, DateOnly, CancellationToken), so a warmed window is recorded as covered exactly as a user-triggered range fetch would be - including for windows the source returns no rows for - and later range lookups of the window are cache hits until normal expiry.

Exceptions

ObjectDisposedException

Thrown when the provider has been disposed.

ArgumentNullException

Thrown when pairs is null.

ArgumentException

endDate precedes startDate.

OperationCanceledException

Thrown when cancellationToken is cancelled.

Applies to

ProductVersions
.NET8, 10