Financial dependency injection
The optional Bodu.Financial.DependencyInjection companion package wires the Bodu.Financial stack into a Microsoft.Extensions.DependencyInjection container. A single AddFinancialService(...) call registers the currency-lookup service and hands back a fluent IFinancialServiceBuilder on which you compose currency lookups, named monetary contexts, and exchange-rate providers. JSON registration is not part of this package - it is the AddFinancialJson extension in the companion Bodu.Financial.Serialization.Json package (see Consuming the financial JSON options). The registration extension methods live in the Bodu.Financial namespace, so a single using Bodu.Financial; brings them into scope.
If you are constructing the financial types by hand - in a console app or a test - keep using the Bodu.Financial constructors directly; this page is only relevant when you want the host to compose the stack for you.
Install
dotnet add package Bodu.Financial.DependencyInjection
The package references Bodu.Financial and Bodu.Core, plus Microsoft.Extensions.DependencyInjection.Abstractions, Microsoft.Extensions.Options, Microsoft.Extensions.Options.ConfigurationExtensions, Microsoft.Extensions.Configuration.Abstractions, and Microsoft.Extensions.Configuration.Binder (for the IConfiguration binding overload).
The registration surface
The entry point is the AddFinancialService IServiceCollection extension (in the Bodu.Financial namespace), with two overloads. Both register the default ICurrency lookup and return an IFinancialServiceBuilder.
| Method | Registers |
|---|---|
AddFinancialService(IServiceCollection, IConfiguration?, string sectionName = "Financial") |
The currency lookup, and binds FinancialOptions (currently an empty options class - see Binding options from configuration) from the named configuration section. The sectionName constant is ServiceCollectionExtensions.DefaultConfigurationSection. |
AddFinancialService(IServiceCollection, Action<IFinancialServiceBuilder> configure) |
The same, with the builder configured imperatively by the delegate. |
Composing the builder
The chainable IFinancialServiceBuilder extension methods (in the Bodu.Financial namespace) add the rest of the stack:
| Builder method | Effect |
|---|---|
AddCurrencyLookup<TLookup>() |
Replaces the default currency lookup with your ICurrencyLookup implementation. |
AddMonetaryContext(string name, MonetaryContext context) |
Registers a named monetary context (rounding, minor units, formatting). |
AddExchangeRateProvider<TProvider>() / AddExchangeRateProvider(provider) |
Registers a timeless IRateProvider, by type or by instance. |
AddDatedExchangeRateProvider<TProvider>() / AddDatedExchangeRateProvider(provider) |
Registers a dated IDatedRateProvider. |
using Bodu.Financial;
builder.Services.AddFinancialService(configure: financial =>
{
financial
.AddExchangeRateProvider<MyRateProvider>()
.AddDatedExchangeRateProvider<HistoricalRateProvider>();
});
The delegate overload is sugar over the first form - AddFinancialService() followed by calls on the returned builder produces the same registrations, so chain directly when that reads better:
services
.AddFinancialService()
.AddDatedExchangeRateProvider<HistoricalRateProvider>();
Named monetary contexts
AddMonetaryContext(name, context) registers a MonetaryContext as a keyed singleton, so an application can carry several rounding regimes side by side - for example a settlement context that follows banker's rounding and a cash-desk context that snaps to the currency's cash increment:
services.AddFinancialService(financial =>
{
financial
.AddMonetaryContext("Settlement", MonetaryContext.Default)
.AddMonetaryContext("CashDesk", new MonetaryContext
{
Rounding = MidpointRoundingStrategy.AwayFromZero,
CashRounding = CashRoundingPolicy.CurrencyCashIncrement,
});
});
Resolve a named context with the standard keyed-service surface:
public sealed class CashDeskService
{
private readonly MonetaryContext _context;
public CashDeskService([FromKeyedServices("CashDesk")] MonetaryContext context) =>
_context = context;
}
// …or imperatively from a built provider:
MonetaryContext cashDesk = provider.GetRequiredKeyedService<MonetaryContext>("CashDesk");
The name must be non-empty; AddMonetaryContext throws ArgumentException for a blank name and ArgumentNullException for a null context.
Registering exchange-rate providers
The generic overloads register an implementation type; the instance overloads accept a pre-built provider. Both use TryAdd semantics, so the first registration for each contract wins:
using Bodu.Financial;
using Bodu.Financial.ExchangeRates;
services.AddFinancialService(financial =>
{
financial.AddDatedExchangeRateProvider(new FixedDatedRateProvider(ecbObservations));
});
To group several providers behind one registration - prioritised fallback, averaging, or per-FX-pair routing - and add read-through caching, use AddAggregatedRateProvider(...) from the Bodu.Financial.ExchangeRates.Caching package (its DI registration ships in the package, in the Bodu.Financial.ExchangeRates namespace), which registers an AggregatingRateProvider as the application's single IDatedRateProvider. RateLookupResult.Rate.Provider records which source answered, so the audit trail survives the composition. See the caching and aggregating guide for the full walkthrough.
Neither AddFinancialService overload registers an FX provider by default - an application that never crosses currencies pays nothing for the contract.
Consuming the financial JSON options
Financial JSON registration ships in the companion Bodu.Financial.Serialization.Json package as a plain IServiceCollection extension. services.AddFinancialJson(policy) registers a configured JsonSerializerOptions as a keyed singleton under FinancialJsonServiceCollectionExtensions.JsonOptionsKey ("Financial"), with the financial converters applied for the chosen FinancialJsonPolicy:
using Bodu.Financial.Serialization.Json;
services.AddFinancialJson(FinancialJsonPolicy.Strict);
JsonSerializerOptions financialJson =
provider.GetRequiredKeyedService<JsonSerializerOptions>(
FinancialJsonServiceCollectionExtensions.JsonOptionsKey);
string payload = JsonSerializer.Serialize(new Money<USD>(19.99m), financialJson);
Binding options from configuration
Passing an IConfiguration binds FinancialOptions from the named section (default "Financial"). The type currently declares no settings of its own - it is the binding seam for future options:
builder.Services.AddFinancialService(builder.Configuration);
Activating static currency resolution
Bodu.Financial exposes a static currency-resolution surface used by parsing and formatting. After the container is built, call UseCurrencyResolution (an IServiceProvider extension in the Bodu.Financial namespace) once so the resolved ICurrencyLookup backs that static surface:
var app = builder.Build();
app.Services.UseCurrencyResolution();
This is a composition-root operation - it installs the container's lookup as the process-wide ambient default via CurrencyResolution.SetDefault. Omitting the call leaves the registry-backed default in place, so existing applications behave identically without it. Only the runtime-tagged Money consults the ambient lookup; Money<TCurrency> reads its precision from the currency tag and is unaffected.
End-to-end with the Generic Host
A complete wiring - host builder, financial registration, and a service that consumes the dated provider through constructor injection:
using Bodu.Financial;
using Bodu.Financial.Currencies;
using Bodu.Financial.ExchangeRates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddFinancialService(builder.Configuration) // binds the "Financial" section
.AddMonetaryContext("Settlement", MonetaryContext.Default)
.AddDatedExchangeRateProvider(new FixedDatedRateProvider(observations));
builder.Services.AddFinancialJson(); // Bodu.Financial.Serialization.Json companion
builder.Services.AddSingleton<SettlementService>();
IHost host = builder.Build();
host.Services.UseCurrencyResolution(); // ambient lookup = the DI lookup
SettlementService settlement = host.Services.GetRequiredService<SettlementService>();
The consuming service depends only on the contract:
public sealed class SettlementService
{
private readonly IDatedRateProvider _rates;
public SettlementService(IDatedRateProvider rates) =>
_rates = rates;
public Money<EUR> Settle(Money<USD> amount, DateOnly postingDate)
{
RateLookupResult lookup = _rates.GetRate(
"USD", "EUR", postingDate,
RateLookupOptions.PreviousWithin(3));
return amount.Convert<EUR>(lookup.Rate.Rate);
}
}
Swapping the fixed table for a live feed later means changing one registration; SettlementService never sees the difference.
Swapping in a test double
Because consumers depend on IDatedRateProvider rather than a concrete feed, tests substitute a deterministic table - FixedDatedRateProvider over hand-written observations is usually all the fake you need:
using Bodu.Financial;
using Bodu.Financial.Currencies;
using Bodu.Financial.ExchangeRates;
using Microsoft.Extensions.DependencyInjection;
ServiceCollection services = new();
services.AddFinancialService(financial =>
{
financial.AddDatedExchangeRateProvider(new FixedDatedRateProvider(new ExchangeRate[]
{
new(CurrencyCode.USD, CurrencyCode.EUR, new DateOnly(2024, 6, 14), 0.928m, "Test"),
}));
});
services.AddSingleton<SettlementService>();
using ServiceProvider provider = services.BuildServiceProvider();
SettlementService sut = provider.GetRequiredService<SettlementService>();
// sut.Settle(new Money<USD>(100m), new DateOnly(2024, 6, 14)) → EUR 92.80
Two registration details matter for tests:
- The provider registrations use
TryAddsemantics - the first registration for a contract wins. Register the fake before any production wiring runs, or useservices.Replace(ServiceDescriptor.Singleton<IDatedRateProvider>(fake))(fromMicrosoft.Extensions.DependencyInjection.Extensions) to override an existing registration. UseCurrencyResolutionmutates process-wide ambient state. Avoid calling it in unit tests; if a test must exercise a custom ambient lookup, prefer the flow-scopedCurrencyResolution.PushScoped(...)fromBodu.Financial, which restores the previous lookup on dispose and isolates parallel tests.
See also
- Working with
Money<TCurrency>- the monetary type that the resolved services back. - Working with exchange rates - the FX provider contracts you register above.
- Exchange-rate types - a usage-scenario catalogue - choosing between the provider implementations.
- Bodu.Financial guides - the member overview for this package.
- Numerics & Financial topic guides - every guide in the topic.
- Numerics & Financial topic overview - package boundaries and the decision table.
IFinancialServiceBuilder·FinancialOptions- the builder and bound options (inBodu.Financial).- Bodu.Financial API reference - full namespace overview; the
AddFinancialService/ builder /UseCurrencyResolutionextension methods live in theBodu.Financialnamespace.