Table of Contents

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 TryAdd semantics - the first registration for a contract wins. Register the fake before any production wiring runs, or use services.Replace(ServiceDescriptor.Singleton<IDatedRateProvider>(fake)) (from Microsoft.Extensions.DependencyInjection.Extensions) to override an existing registration.
  • UseCurrencyResolution mutates process-wide ambient state. Avoid calling it in unit tests; if a test must exercise a custom ambient lookup, prefer the flow-scoped CurrencyResolution.PushScoped(...) from Bodu.Financial, which restores the previous lookup on dispose and isolates parallel tests.

See also