Integration — ASP.NET Core, Pragmatic Host, EF Core
How to wire Pragmatic.Internationalization into a web application: DI registration, the culture
middleware, the fluent I18NBuilder, the translation endpoint, localized ProblemDetails, and
EF Core persistence.
ASP.NET Core (plain)
Section titled “ASP.NET Core (plain)”When you need it
Section titled “When you need it”Any web app that must resolve the request culture and format/localize accordingly.
What you write
Section titled “What you write”builder.Services.AddPragmaticInternationalization(options =>{ options.DefaultUICulture = CultureCode.EnglishUS; options.SupportedCultures = [CultureCode.EnglishUS, CultureCode.Italian];});
var app = builder.Build();app.UsePragmaticInternationalization(); // culture middleware, before routingOr bind options from configuration (section "I18N" by default):
builder.Services.AddPragmaticInternationalization(builder.Configuration);What you get
Section titled “What you get”AddPragmaticInternationalization registers:
| Service | Lifetime | Role |
|---|---|---|
SystemConfigProvider | singleton | fallback config provider (Priority 0) reading I18NOptions |
I18NConfigResolver | scoped | merges all II18NConfigProviders by priority |
GlobalizationFormatter | scoped | culture-aware formatting, follows I18NContext.Current |
| JSON converters | — | all i18n types on ConfigureHttpJsonOptions (see below) |
It returns an I18NBuilder for further fluent configuration (see below).
UsePragmaticInternationalization() adds I18NContextMiddleware, which per request:
- Resolves the configuration from the provider chain (
I18NConfigResolver). - Applies a request culture override: query string (
options.QueryStringKey, defaultculture) first, thenAccept-Language(quality-ordered best match) — both validated againstSupportedCultures. - Sets
I18NContext(and thread cultures), opens anI18N.Context.Resolveactivity. - Captures and restores the previous thread cultures after the request — no culture leaks between requests on the same thread.
Pragmatic Host
Section titled “Pragmatic Host”When you need it
Section titled “When you need it”In a Pragmatic.Composition host — the middleware is auto-wired for you by
InternationalizationStep (an IStartupStep with Order = 40, before routing).
What you write
Section titled “What you write”await PragmaticApp.RunAsync(args, app =>{ app.UseI18N(i18n => { i18n.DefaultCulture(CultureCode.EnglishUS) .Support(CultureCode.EnglishUS, CultureCode.Italian); i18n.AddJsonTranslations("localization", watchForChanges: true); i18n.LocalizeProblemDetails(); });});What you get
Section titled “What you get”UseI18N calls AddPragmaticInternationalization() and hands you the same I18NBuilder.
No manual UsePragmaticInternationalization() needed — the startup step adds the middleware.
I18NBuilder Reference
Section titled “I18NBuilder Reference”| Method | Effect |
|---|---|
DefaultCulture(CultureCode) | sets I18NOptions.DefaultUICulture |
Support(params CultureCode[]) | sets I18NOptions.SupportedCultures |
AddScope<TScope>() | registers a typed ICultureScope default culture |
AddConfigProvider<TProvider>() | registers an II18NConfigProvider (scoped) |
AddConfigProvider(II18NConfigProvider) | registers a provider instance (singleton) |
AddConfigProvider(Func<IServiceProvider, II18NConfigProvider>) | registers a provider factory (scoped) |
AddLocalizationProvider<TProvider>() | registers an ILocalizationProvider (singleton) |
AddLocalizationProvider(ILocalizationProvider) | registers a provider instance |
AddJsonTranslations(basePath?, watchForChanges) | registers JsonLocalizationProvider (default path: I18NOptions.TranslationsPath, "translations") |
LocalizeProblemDetails() | replaces IErrorMessageResolver with LocalizedErrorMessageResolver |
Configuration Providers (per-tenant / per-user culture)
Section titled “Configuration Providers (per-tenant / per-user culture)”When you need it
Section titled “When you need it”The culture must come from data — the tenant’s settings, the user profile — not from static options.
What you write
Section titled “What you write”Implement II18NConfigProvider with a priority above 0; derive from CachedConfigProvider
to get TTL caching (default 5 minutes) and invalidation for free:
public sealed class TenantConfigProvider(ITenantContext tenant) : CachedConfigProvider{ public override int Priority => 100; // beats SystemConfigProvider (0) protected override string GetCacheKey() => tenant.Id; protected override I18NConfig? LoadConfiguration() => new() { DefaultUICulture = tenant.Culture }; // null properties = defer to lower priority}i18n.AddConfigProvider<TenantConfigProvider>();What you get
Section titled “What you get”I18NConfigResolver merges all providers highest-priority-first; each null property in an
I18NConfig defers to the next provider down, ending at SystemConfigProvider (the options).
Suggested priority convention: request 300 · user 200 · tenant 100 · system 0 (only the system
provider is built in; the others are yours).
Translation Endpoint (frontend feed)
Section titled “Translation Endpoint (frontend feed)”When you need it
Section titled “When you need it”A SPA/frontend needs the same translations the backend knows, without duplicating JSON files.
What you write
Section titled “What you write”app.MapPragmaticTranslations(); // GET /api/i18n/{culture}app.MapPragmaticTranslations("/api/translations", cacheDurationSeconds: 600);What you get
Section titled “What you get”GET /api/i18n/it returns all merged translations from every registered
ILocalizationProvider (via CompositeLocalizationProvider) for that culture, as a flat
key → value map. Supports ?prefix=error.validation for lazy-loading a subset per
boundary/page. Responses carry Cache-Control: public, max-age=300 (configurable). Returns
500 if no localization provider is registered.
Localized ProblemDetails
Section titled “Localized ProblemDetails”When you need it
Section titled “When you need it”Domain errors (Result failures) must reach the client in the caller’s language.
What you write
Section titled “What you write”i18n.LocalizeProblemDetails();Translation keys follow the error’s MessageKey (or, when resolving from a bare code,
error. + the code lowercased with _ → .):
{ "error.room.unavailable": "Room is not available for the selected dates", "error.room.unavailable.title": "Room Not Available"}What you get
Section titled “What you get”LocalizedErrorMessageResolver resolves ProblemDetails detail from the error’s message key
and title from {messageKey}.title, in the current request culture, with {param}
placeholders interpolated from Error.Parameters ("Cannot exceed {limit} nights" +
Parameters["limit"] = 14 → "Cannot exceed 14 nights"). Missing keys fall back to the
error’s own message.
JSON Converters
Section titled “JSON Converters”Registered automatically on ASP.NET Core’s JSON options by AddPragmaticInternationalization.
For a custom JsonSerializerOptions:
var options = new JsonSerializerOptions();options.AddPragmaticInternationalization();Covered types (plus their nullable variants): Money ({ "amount": 99.99, "currency": "USD" }),
CurrencyCode, LanguageCode, CountryCode, CultureCode (as ISO/BCP-47 strings),
LocalizedString (as a { "en": "...", "it": "..." } map).
Validation Attributes
Section titled “Validation Attributes”DataAnnotations for money DTOs — no registration needed:
public sealed record CreateInvoiceRequest{ [Required, PositiveMoney] // Amount > 0 (null passes; add [Required]) public Money? Total { get; init; }
[NonNegativeMoney] // Amount >= 0 public Money? Discount { get; init; }
[SupportedCurrency("EUR", "USD")] // whitelist (Money or CurrencyCode) public CurrencyCode Currency { get; init; }}EF Core
Section titled “EF Core”What you write
Section titled “What you write”Either per-context:
protected override void OnModelCreating(ModelBuilder modelBuilder){ modelBuilder.ApplyPragmaticInternationalization();}or convention-based (EF Core 6+):
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder){ configurationBuilder.ApplyPragmaticInternationalizationConventions();}What you get
Section titled “What you get”| CLR type | Column mapping |
|---|---|
LocalizedString | JSON string ({"en":"...","it":"..."}), empty on null/empty |
CurrencyCode / CurrencyCode? | varchar(3) ISO code |
Money: manual mapping
Section titled “Money: manual mapping”Money has no automatic converter — a single-column mapping would lose either precision
or the currency. Map it as two columns; MoneyConfiguration.DefaultPrecision (19) and
MoneyConfiguration.DefaultScale (4) are the recommended constants:
builder.OwnsOne(i => i.Total, money =>{ money.Property(m => m.Amount).HasPrecision(MoneyConfiguration.DefaultPrecision, MoneyConfiguration.DefaultScale); money.Property(m => m.Currency).HasMaxLength(3);});Observability
Section titled “Observability”The module emits OpenTelemetry-friendly signals, all named under Pragmatic.Internationalization:
- ActivitySource — the middleware opens an
I18N.Context.Resolveactivity per request, tagged with the resolved cultures. - Meter — counters
pragmatic.i18n.key_lookupsandpragmatic.i18n.missing_keystrack translation lookups and misses at runtime (StringLocalizer).
Enable richer tracing with I18NOptions.EnableDiagnostics = true, and subscribe your OTel
setup to the Pragmatic.Internationalization source/meter names.
See Also
Section titled “See Also”- Getting Started — culture context, Money, formatting
- Translation Keys — the generated
Tclass and localization providers - Troubleshooting — diagnostics and common failures
Pragmatic.Temporal.Internationalization— culture-aware formatting for the Temporal types (LocalDateTime,ZonedDateTime);LocalDate/LocalTimework out of the box viaDateOnly/TimeOnlyconversions