CachingRateProviderBase Class
Definition
- Namespace
- Bodu.Financial.ExchangeRates.Caching
- Assembly
- Bodu.Financial.ExchangeRates.Caching.dll
- Package
- Bodu.Financial.ExchangeRates.Caching 1.0.0
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
cacheIRateCacheThe single-provider cache that serves fresh rates and stores resolved observations.
optionsCachingRateOptionsThe options carrying the caching durations.
timeProviderTimeProviderThe time source used to evaluate freshness and stamp newly cached rows. null selects System.
loggerILoggerThe logger that records cache hits, misses, and refetches. null selects Instance.
ownsInnerbooltrue 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
cacheoroptionsis null.- ArgumentException
Thrown when
optionsfails 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
fromIsoCodestringThe source currency's ISO 4217 code.
toIsoCodestringThe 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
fromIsoCodestringThe source-currency ISO-style code.
toIsoCodestringThe destination-currency ISO-style code.
optionsRateLookupOptionsThe lookup rules to apply. null selects the implementation's default most-recent policy.
Returns
- RateLookupResult
The resolved RateLookupResult.
Exceptions
- ArgumentNullException
Thrown if
fromIsoCodeortoIsoCodeis null.- ArgumentException
Thrown if either ISO code is not a three-character uppercase ASCII code, or if
optionsis 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
fromIsoCodestringThe source-currency ISO-style code.
toIsoCodestringThe destination-currency ISO-style code.
dateDateOnlyThe calendar date for which a rate is required.
optionsRateLookupOptionsThe lookup rules to apply, including date-resolution policy and tolerance. null is treated as Exact.
Returns
- RateLookupResult
The resolved RateLookupResult.
Exceptions
- ArgumentNullException
Thrown if
fromIsoCodeortoIsoCodeis null.- ArgumentException
Thrown if either ISO code is not a three-character uppercase ASCII code, or if
optionsis invalid (for example, Exact with non-zero tolerance).- ArgumentOutOfRangeException
Thrown if
optionscontains 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
fromIsoCodestringThe source-currency ISO-style code.
toIsoCodestringThe destination-currency ISO-style code.
optionsRateLookupOptionsThe lookup rules to apply. null selects the implementation's default most-recent policy.
cancellationTokenCancellationTokenA token to observe while awaiting the operation.
Returns
- ValueTask<RateLookupResult>
A task that yields the resolved RateLookupResult.
Exceptions
- ArgumentNullException
Thrown if
fromIsoCodeortoIsoCodeis null.- ArgumentException
Thrown if either ISO code is not a three-character uppercase ASCII code, or if
optionsis 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
fromIsoCodestringThe source-currency ISO-style code.
toIsoCodestringThe destination-currency ISO-style code.
dateDateOnlyThe calendar date for which a rate is required.
optionsRateLookupOptionsThe lookup rules to apply, including date-resolution policy and tolerance. null is treated as Exact.
cancellationTokenCancellationTokenA token to observe while awaiting the operation.
Returns
- ValueTask<RateLookupResult>
A task that yields the resolved RateLookupResult.
Exceptions
- ArgumentNullException
Thrown if
fromIsoCodeortoIsoCodeis null.- ArgumentException
Thrown if either ISO code is not a three-character uppercase ASCII code, or if
optionsis invalid.- ArgumentOutOfRangeException
Thrown if
optionscontains 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
fromIsoCodestringThe source-currency ISO-style code.
toIsoCodestringThe destination-currency ISO-style code.
startDateDateOnlyThe inclusive start of the range.
endDateDateOnlyThe 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
fromIsoCodeortoIsoCodeis null.- ArgumentException
Thrown if either ISO code is not a three-character uppercase ASCII code, or if
endDateprecedesstartDate.
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
fromIsoCodestringThe source-currency ISO-style code.
toIsoCodestringThe destination-currency ISO-style code.
startDateDateOnlyThe inclusive start of the range.
endDateDateOnlyThe inclusive end of the range.
cancellationTokenCancellationTokenA 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
fromIsoCodeortoIsoCodeis null.- ArgumentException
Thrown if either ISO code is not a three-character uppercase ASCII code, or if
endDateprecedesstartDate.
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
fromIsoCodestringThe source-currency ISO-style code.
toIsoCodestringThe destination-currency ISO-style code.
dateDateOnlyThe calendar date for which a rate is required.
optionsRateLookupOptionsThe lookup rules to apply, including date-resolution policy and tolerance. null is treated as Exact.
resultRateLookupResultWhen this method returns true, contains the resolved RateLookupResult; otherwise, contains default.
Returns
Exceptions
- ArgumentNullException
Thrown if
fromIsoCodeortoIsoCodeis null.- ArgumentException
Thrown if either ISO code is not a three-character uppercase ASCII code, or if
optionsis invalid.- ArgumentOutOfRangeException
Thrown if
optionscontains 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
pairsIEnumerable<(string From, string To)>The currency pairs to warm, each a from/to ISO-code tuple.
startDateDateOnlyThe inclusive first date of the window to warm.
endDateDateOnlyThe inclusive last date of the window to warm.
cancellationTokenCancellationTokenCancels the warm-up.
Returns
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
pairsis null.- ArgumentException
endDateprecedesstartDate.- OperationCanceledException
Thrown when
cancellationTokenis cancelled.
Applies to
| Product | Versions |
|---|---|
| .NET | 8, 10 |