Bodu.Financial.ExchangeRates.Caching
1.0.0
dotnet add package Bodu.Financial.ExchangeRates.Caching --version 1.0.0
NuGet\Install-Package Bodu.Financial.ExchangeRates.Caching -Version 1.0.0
<PackageReference Include="Bodu.Financial.ExchangeRates.Caching" Version="1.0.0" />
<PackageVersion Include="Bodu.Financial.ExchangeRates.Caching" Version="1.0.0" />
<PackageReference Include="Bodu.Financial.ExchangeRates.Caching" />
paket add Bodu.Financial.ExchangeRates.Caching --version 1.0.0
#r "nuget: Bodu.Financial.ExchangeRates.Caching, 1.0.0"
#:package Bodu.Financial.ExchangeRates.Caching@1.0.0
#addin nuget:?package=Bodu.Financial.ExchangeRates.Caching&version=1.0.0
#tool nuget:?package=Bodu.Financial.ExchangeRates.Caching&version=1.0.0
Bodu.Financial.ExchangeRates.Caching
API stability — Stable. The public API surface is committed; breaking changes are reserved for a major-version bump per SemVer.
A caching and composition layer for Bodu.Financial exchange-rate providers.
For the full walkthrough — quickstart, stacking (tiered read-through), aggregation, "when to use which", observability, and troubleshooting — see the Caching and aggregating exchange rates guide.
The provider classes (Yahoo, RBA, ECB, BoE) are pure fetchers — they know nothing
of caching. This package adds two orthogonal pieces that each implement the same
IDatedRateProvider contract (and the timeless IRateProvider), so
they compose anywhere a provider is expected:
Caller
│ IDatedRateProvider / IRateProvider
▼
AggregatingRateProvider ── groups named children; routes per FX pair and
│ combines them with a strategy (priority / average)
├── CachingRateProvider("RBA") ── read-through cache over ONE source + ONE cache
│ └── RbaRateProvider
└── CachingRateProvider("ECB")
└── EcbRateProvider
Caching (one cache = one provider)
CachingRateProvider wraps exactly one inner IDatedRateProvider
over one single-provider IRateCache. On a lookup it first tries the cache's
fresh rows (reusing FixedDatedRateProvider for date-resolution, inverse, and
identity handling) and, on a miss, delegates to the inner provider and stores what it
returns. It also exposes the timeless IRateProvider.GetRate(from, to), which
resolves the current UTC date under CachingRateOptions.DefaultLookupOptions.
The cache is bound to a single provider, so its surface carries no provider argument:
| Type | Role |
|---|---|
IRateCache |
Single-provider cache contract (Provider; GetRates/Store by pair). |
RateCacheBase<TOptions> |
Storage-agnostic core: freshness filtering + merge/prune. No physical layout. |
IFileRateCache |
File-storage seam (CacheDirectory, ResolveFilePath, ResolveDirectory, ResolvePartitionPath). |
FileRateCacheBase<TOptions> |
File plumbing: layout-driven directory + file-name resolution, date partitioning, best-effort IO. |
TomlFileRateCache |
Sealed TOML leaf — <dir>/<provider>/<from><to>.toml, decimals quoted for lossless round-trips, self-describing Provider/From/To header. |
JsonFileRateCache |
Sealed JSON leaf — <dir>/<provider>/<from><to>.json, decimals as JSON numbers, the same self-describing header. |
RateCacheFileLayout |
Where a pair's files live and whether they split by date: SingleFile (default), Yearly, Monthly, Daily, or Create(strategy, directoryFunc?, fileNameFunc?). |
RateCachePartitionStrategy |
The date split a layout applies: Single, Yearly, Monthly, Daily, or Custom(...). |
InMemoryRateCache |
In-memory cache reusing the same expiry mechanism; nothing persisted. |
NullRateCache |
No-op cache (NullRateCache.Create(provider)). |
CachingRateProvider |
Read-through caching decorator over one source + one cache. |
CachingRateOptions |
Cache location, default + per-provider expiry, log levels, timeless lookup options. |
Craft your own storage by implementing IRateCache, extending
RateCacheBase<TOptions> (storage-agnostic), or extending
FileRateCacheBase<TOptions> (a new file format).
File layout and date partitioning
Both file caches store one file per pair by default
(<dir>/<provider>/<from><to>.toml or .json) and write a self-describing
Provider/From/To header into each file, so a file no longer depends on its
name or folder for identity. Set FileRateCacheOptions.Layout to control
the folder hierarchy, file name, and whether a pair's rows split across files by
date — RateCacheFileLayout.Yearly / .Monthly / .Daily write one file
per calendar period under a per-pair folder (for example
<dir>/RBA/AUDUSD/2023-01.toml), and RateCacheFileLayout.Create(...)
builds a custom layout from a partition strategy and optional directory/file-name
delegates:
var monthly = new TomlFileRateCache(new FileRateCacheOptions
{
Provider = "RBA",
CacheDirectory = "/var/cache/fx",
Layout = RateCacheFileLayout.Monthly,
});
// JSON instead of TOML, same layout/partitioning surface.
var json = new JsonFileRateCache(new FileRateCacheOptions
{
Provider = "RBA",
CacheDirectory = "/var/cache/fx",
});
Aggregation (group many providers behind one entry point)
AggregatingRateProvider groups several named children and resolves each request
through a pluggable IRateAggregationStrategy, with optional per-FX-pair routing.
PriorityFallbackStrategy— first child that resolves wins (the default).AverageStrategy— arithmetic mean of every child that resolves, taggedAverage.- Implement
IRateAggregationStrategyfor your own (weighted, median, …). RateAggregationOptions.Routesmaps a pair to an ordered child list and an optional per-pair strategy, soAUD/USDcan prefer[RBA, ECB]whileUSD/GBPprefers[ECB, RBA].TryGetProvider(name, out provider)resolves a specific child directly.
// The caching provider is storage-agnostic — you supply the IRateCache.
var rba = new CachingRateProvider(
rbaSource, new TomlFileRateCache(new FileRateCacheOptions { Provider = "RBA", CacheDirectory = "/var/cache/fx" }), options);
var ecb = new CachingRateProvider(
ecbSource, new TomlFileRateCache(new FileRateCacheOptions { Provider = "ECB", CacheDirectory = "/var/cache/fx" }), options);
var agg = new RateAggregationOptions();
agg.Routes[new CurrencyPair(CurrencyCode.AUD, CurrencyCode.USD)] = new CurrencyPairRoute(new[] { "RBA", "ECB" });
agg.Routes[new CurrencyPair(CurrencyCode.USD, CurrencyCode.GBP)] = new CurrencyPairRoute(new[] { "ECB", "RBA" });
IDatedRateProvider provider = new AggregatingRateProvider(
new[]
{
new NamedDatedRateProvider("RBA", rba),
new NamedDatedRateProvider("ECB", ecb),
},
agg);
For dependency-injection wiring, see
Bodu.Financial.ExchangeRates.Caching.DependencyInjection.
Logging
Both the caching decorator and the aggregator log through Microsoft.Extensions.Logging.
Pass an ILogger to the constructor, or let the *.DependencyInjection package wire one
for you. When no logger is supplied it defaults to NullLogger.Instance, so logging is
entirely opt-in and free when unused.
Single-date lookups happen on the read hot path, so their hit/miss diagnostics default to
Trace; coarser range operations default to Debug. Every level is individually
configurable on CachingRateOptions:
| Event | Default level | Option property |
|---|---|---|
| A single-date lookup served from the cache | Trace |
CacheHitLogLevel |
| A single-date cache miss resolved from a source and cached | Trace |
CacheMissLogLevel |
| A range lookup served entirely from the cache | Debug |
CacheRangeHitLogLevel |
| A range lookup refetched from a source and re-cached | Debug |
CacheRangeRefetchLogLevel |
The aggregator's route-selected, aggregated, and unresolved diagnostics are configurable on
RateAggregationOptions.
Persistent backends may add their own diagnostics. The SQLite cache logs best-effort storage
degradation at Warning under EventId 4520 (first failure immediately, then rate-limited
to one per minute) so a silently-degrading cache is visible — see the
Bodu.Financial.ExchangeRates.Caching.Sqlite
package and the observability section of the
caching guide.
Served-rate provenance & data age
Every RateLookupResult carries a RateProvenance describing where the rate came from:
Origin == Live— the value was resolved directly by a provider (for a cache-fronted provider, a miss the inner provider satisfied).Backend,CachedAtUtc, andAgeare allnull.Origin == Cache— the value was served from a cache without consulting the provider.Backendis the cache's runtime identity,CachedAtUtcis the instant the served data was written to the cache, andAgeis the elapsed time since then, clamped to be never negative (Age >= 0, since a row may be written marginally ahead of the lookup clock).
Backend is a diagnostic runtime identity (the cache type's name), not a stable key — do not parse it or branch on
it as if it were part of the contract.
Two ages travel with a cache-served rate and are deliberately distinct:
- Cache-write age —
Provenance.Age(andProvenance.CachedAtUtc) is anchored to when the row was written to the cache. - Data age —
ExchangeRate.FetchedAtUtccarries the upstream fetch instant end to end. A provider stamps it when it loads a rate; the cache persists it (asCachedRate.ObservedAtUtc) and restores it onto the rate it serves, so a cache-served rate reports the original fetch instant. Data age isnow - ExchangeRate.FetchedAtUtc, independent of how recently the row happened to be (re)written to the cache.
FetchedAtUtc is excluded from ExchangeRate equality. The TOML and JSON file caches persist it as an optional
ObservedAtUtc entry key per row; an entry written before the instant was tracked (or whose source never supplied one)
has no key and reads back null. The SQLite and distributed caches persist it the same way through their own additive
fields.
Part of the Bodu utility library.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Bodu.Core (>= 1.0.0)
- Bodu.Financial (>= 1.0.0)
- Bodu.Financial.DependencyInjection (>= 1.0.0)
- Bodu.Text.Toml (>= 1.0.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.12)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Options (>= 10.0.12)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.12)
-
net8.0
- Bodu.Core (>= 1.0.0)
- Bodu.Financial (>= 1.0.0)
- Bodu.Financial.DependencyInjection (>= 1.0.0)
- Bodu.Text.Toml (>= 1.0.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Configuration.Binder (>= 8.0.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Options (>= 8.0.2)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Bodu.Financial.ExchangeRates.Caching:
| Package | Downloads |
|---|---|
|
Bodu.Financial.ExchangeRates.Caching.Distributed
Distributed (Redis-capable) exchange-rate cache for Bodu.Financial. Provides DistributedRateCache, an IRateCache implementation over Microsoft.Extensions.Caching.Distributed.IDistributedCache that persists a single provider's rates and fetch-coverage windows as a per-pair JSON blob, with the same freshness, merge, coverage, and validation semantics as the in-memory, TOML, and SQLite caches. Includes the dependency-injection registrations (AddDistributedRateCache, AddRedisRateCache) on IFinancialServiceBuilder. |
|
|
Bodu.Financial.ExchangeRates.Caching.Sqlite
SQLite-backed exchange-rate cache for Bodu.Financial. Provides SqliteRateCache, an IRateCache implementation that persists a single provider's rates and fetch-coverage windows in a SQLite database, with the same freshness, merge, coverage, and validation semantics as the in-memory and TOML caches. Includes the dependency-injection registration (AddSqliteRateCache) on IFinancialServiceBuilder. |
GitHub repositories
This package is not used by any popular GitHub repositories.