Bodu.Financial.ExchangeRates.Caching.Distributed
1.0.0
dotnet add package Bodu.Financial.ExchangeRates.Caching.Distributed --version 1.0.0
NuGet\Install-Package Bodu.Financial.ExchangeRates.Caching.Distributed -Version 1.0.0
<PackageReference Include="Bodu.Financial.ExchangeRates.Caching.Distributed" Version="1.0.0" />
<PackageVersion Include="Bodu.Financial.ExchangeRates.Caching.Distributed" Version="1.0.0" />
<PackageReference Include="Bodu.Financial.ExchangeRates.Caching.Distributed" />
paket add Bodu.Financial.ExchangeRates.Caching.Distributed --version 1.0.0
#r "nuget: Bodu.Financial.ExchangeRates.Caching.Distributed, 1.0.0"
#:package Bodu.Financial.ExchangeRates.Caching.Distributed@1.0.0
#addin nuget:?package=Bodu.Financial.ExchangeRates.Caching.Distributed&version=1.0.0
#tool nuget:?package=Bodu.Financial.ExchangeRates.Caching.Distributed&version=1.0.0
Bodu.Financial.ExchangeRates.Caching.Distributed
API stability — Stable. The public API surface is committed; breaking changes are reserved for a major-version bump per SemVer.
A distributed (Redis-capable) cache for Bodu.Financial exchange-rate providers.
One
IRateCachebackend among several. For the composition model, theSqliteRateCachealternative, and "when to use which", see the Caching and aggregating exchange rates guide.
DistributedRateCache implements the IRateCache contract over a
Microsoft.Extensions.Caching.Distributed.IDistributedCache, persisting one provider's dated rates and fetch-coverage
windows so they need not be re-fetched while fresh. It is behaviourally identical to the in-memory, TOML, and SQLite
caches in Bodu.Financial.ExchangeRates.Caching — the same freshness, merge, coverage, and validation semantics — and
is validated against the same shared RateCacheContractTests.
Because it depends only on the IDistributedCache abstraction it is fully unit-testable in-memory (against
MemoryDistributedCache) and, in production, backed by Redis (via
Microsoft.Extensions.Caching.StackExchangeRedis) or any other IDistributedCache implementation.
Storage
- One entry per currency pair, under a stable, collision-free key
{prefix}{provider}:{from}{to}. - The value is a single JSON blob carrying both the cached rate rows and the recorded coverage windows.
- Decimal rates are serialized as invariant strings and all dates and instants as invariant ISO text (
yyyy-MM-ddfor dates, round-trip"O"for instants) so precision and scale round-trip losslessly. - Each rate row carries an additive optional
observedAtUtcJSON property holding the upstream fetch instant (ExchangeRate.FetchedAtUtc), distinct from the row's cache-write instant. It is omitted from the JSON whennull; a legacy blob written before the property existed (or a row whose source supplied no fetch instant) reads backnull.
Behaviour
- Expiry is by caching duration: stale and semantically invalid rows are filtered on read and pruned on write; stale coverage windows are pruned when coverage is recorded, so the entry self-cleans.
- The two independent half-writes preserve the other half:
Storewrites rate rows without dropping coverage, andRecordCoveragewrites coverage windows without dropping rows — each by read-modify-writing the per-pair blob. StoreFetchedRange— the path theCachingRateProviderdecorator uses after a range fetch — writes both halves together as one atomic blob set: the pair's rate rows and the fetched coverage window are merged and persisted in a singleSet, all-or-nothing. A reader (even in another process) therefore never observes coverage without its rows, so a range lookup cannot report a false hit and return incomplete data as if complete. The write returns anRateCacheWriteStatus(Storedwhen both halves were persisted,Failedwhen a backing-store error was swallowed and nothing was persisted,Skippedfor a no-op cache), which the decorator logs and, onFailed, treats as a miss so the next lookup refetches rather than trusting partial coverage.- An empty-but-fetched range still records coverage: a successful fetch that returned no observation (a weekend, a holiday, a true gap) marks the window covered, so it is served from the cache on a later lookup rather than being perpetually re-fetched.
- Consistency is best-effort. An
IDistributedCacheoffers no cross-process atomic read-modify-write, so each write reads the per-pair blob, modifies it, and writes it back. Same-process races are prevented by a per-pair in-process lock. Cross-process, the independentStore/RecordCoveragehalf-writes are last-write-wins, while the decorator'sStoreFetchedRangeblob set is atomic per write (each writer persists a self-consistent rows-plus-coverage blob, so a concurrent overwrite loses an update but never tears a half into another writer's blob). A backing-store failure or a corrupt blob degrades to an empty read or skipped write rather than throwing.
Because the persisted observedAtUtc is restored onto a served rate's ExchangeRate.FetchedAtUtc, a cache-served rate
reports its original upstream fetch instant (data age), distinct from the cache-write age surfaced through
RateLookupResult.Provenance (CachedAtUtc / Age). See the served-rate provenance notes in the
Bodu.Financial.ExchangeRates.Caching README.
When to use
Reach for this distributed (Redis-backed) cache when several application instances or processes share one rate cache,
so a fetch by one instance warms the others. For a single process, prefer the SQLite, file (TOML), or in-memory caches
in Bodu.Financial.ExchangeRates.Caching (and …Caching.Sqlite): they offer stronger local atomicity — the per-pair
lock for the in-memory and file caches, and one transaction for SQLite — for every write path, including the independent
Store / RecordCoverage halves.
Usage
var options = new DistributedRateCacheOptions { Provider = "RBA" };
var cache = new DistributedRateCache(distributedCache, options);
IDatedRateProvider cached = new CachingRateProvider(rba, cache, new CachingRateOptions());
Or, through dependency injection (the package ships its own AddDistributedRateCache / AddRedisRateCache registration in the Bodu.Financial.ExchangeRates namespace):
using Bodu.Financial;
using Bodu.Financial.ExchangeRates;
// Over an already-registered IDistributedCache:
services.AddFinancialService()
.AddDistributedRateCache("RBA");
// Or register a Redis IDistributedCache and the cache together:
services.AddFinancialService()
.AddRedisRateCache("RBA", redis => redis.Configuration = "localhost:6379");
| 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.Financial.ExchangeRates.Caching (>= 1.0.0)
- Microsoft.Extensions.Caching.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Caching.StackExchangeRedis (>= 10.0.12)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.12)
- Microsoft.Extensions.DependencyInjection.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.Financial.ExchangeRates.Caching (>= 1.0.0)
- Microsoft.Extensions.Caching.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Caching.StackExchangeRedis (>= 8.0.28)
- Microsoft.Extensions.Configuration.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Configuration.Binder (>= 8.0.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Options (>= 8.0.2)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.