Summary#
Format a number or date for a declared culture, spelled exactly as C#'s IFormattable.ToString(format, provider) —
the provider is a BCP-47 culture token ("sv-SE", "de-DE"), not an ambient setting:
var price = total.ToString("C", "sv-SE"); // "1 234,56 kr" — Swedish krona, space grouping, comma decimal
var pct = rate.ToString("P1", "en-US"); // "12.5 %"
var when = order.CreatedAt.ToString("D", "de-DE"); // "Freitag, 5. Januar 2024"The culture must be declared (a cultures { } set; the platform ships a default pack, so sv-SE/de-DE/… work
with no ceremony). A per-call culture is the override; app.DefaultCulture sets the app-wide default so a culture-less
total.ToString("C") still formats for it.
Signature#
string value.ToString(string format) // the app-default culture (else invariant)
string value.ToString(string format, string culture) // an explicit culture — the per-call override
// declared once, app-wide:
app.DefaultCulture = "sv-SE";
// per-viewer (on the principal entity) + the read:
[PreferredCulture] Culture Locale;
string cur = Session.CurrentCulture; // the current viewer's effective culture tokenDescription#
The culture is explicit, never ambient. There is no "current culture" read from the machine or the request — a value formats for the culture you name, or for the app default. This is what makes a price render the same on the server and in every browser.
Declared, closed set. A culture token on a format call must belong to the app's declared cultures { } set. The
platform ships a default pack (en-US, en-GB, sv-SE, de-DE, fr-FR, es-ES, it-IT, nl-NL, pt-BR,
ja-JP, zh-CN); an app can vendor the block to curate. A token outside the set is a compile error (with a
"did you mean").
Supported formats (run in the browser, no round trip). Over a Decimal, Int32, Double (fixed specifiers only),
DateTime, DateOnly or TimeOnly:
| Kind | Formats | Example (sv-SE) |
|---|---|---|
| Number | N F D (+ digits: N2, F0) | 1 234,56 |
| Currency | C (+ digits) | 1 234,56 kr |
| Percent | P (+ digits) | 12,5 % |
| Date/time | d D t T g G M Y | 2024-01-05 · fredag 5 januari 2024 |
| Custom numeric | #,##0.00, 0.00 | 1 234,56 |
| Custom date | yyyy-MM-dd, dd/MM/yyyy, MMMM d | 2024-01-05 |
The client reproduces .NET exactly — the culture's separators (including a non-breaking-space group separator and a
− minus), currency symbol and placement, month/day names (with the genitive forms .NET selects when a day number
is adjacent, e.g. German 5. Juni vs standalone Jun), and AM/PM.
A Double formats client-side too for the fixed specifiers (N/F/C/P) — the client rounds the value's
exact decimal expansion (a double is a terminating decimal), reproducing .NET including its type-specific rounding
(a double rounds half-to-even, (2.5).ToString("F0") → "2", where a Decimal rounds half-away-from-zero). Prefer
Decimal for money regardless — it is exact end to end.
Everything else runs on the server (correct, one round trip): parsing a string back to a value; the numeric
E/G/R/X specifiers, a custom pattern over a double, and a double's plain .ToString() — these need
shortest-round-trip / 15-digit rounding that isn't reproduced client-side; non-Gregorian date calendars/eras under a
culture; and any format built at runtime.
App default. app.DefaultCulture = "sv-SE"; makes a culture-less value.ToString("C") format for sv-SE
everywhere — server and client — without repeating the culture at each call. A per-call culture still overrides it. With
no default declared, a culture-less format is culture-neutral (invariant), unchanged.
Per-viewer culture. Give the principal entity a Culture property marked [PreferredCulture], and a culture-less
value.ToString("C") renders in the current signed-in user's culture. Culture is a value-kind that stores a BCP-47
token ("de-DE") — a picker or a plain string sets it — validated to be a real culture when written. The resolution
order is: a per-call culture > the viewer's [PreferredCulture] > app.DefaultCulture > invariant. It resolves at
runtime and, like every other supported format, runs in the browser (the viewer's culture is served with the app's
metadata, so there is no round trip); a viewer whose preferred culture is not one the app declared falls back to the
app default.
Reading the viewer's culture. Session.CurrentCulture returns that effective token — the viewer's [PreferredCulture]
if set, else app.DefaultCulture, else "" (invariant) — as a value, so you can branch on it (if (Session.CurrentCulture == "de-DE") …) or pass it on. It is the author-facing sibling of Session.CurrentUser, resolves the same way as the
implicit formatting above, and runs in the browser with no round trip.
Parsing (the inverse). Convert.ToDecimal(s, "sv-SE") reads a number written in a culture ("1 234,56" → 1234.56),
returning 0 for a string it can't parse (the Convert.To* contract). It runs client-side too, byte-identical to
the server, over a defined profile: leading/trailing whitespace, one sign, group separators, and a decimal separator.
Outside that profile — a currency symbol in the string, parentheses for a negative, an exponent — parses server-side (a
round trip). A plain Convert.ToDecimal(s) (no culture) already parses invariantly in the browser.
Convert.ToInt(s, "de-DE") is the integer twin — it runs client-side too, over the same profile minus a decimal
point and minus a trailing sign (a "1234-" is a value for ToDecimal but 0 for ToInt). A value outside the
32-bit integer range parses to 0, like every unparseable string.
Parsing a date exactly. DateTime.ParseExact(s, "yyyy-MM-dd", "sv-SE") reads a date/time written to a specific
pattern — the inverse of the date formatter. It is STRICT: a string that does not match the pattern throws (unlike the
number parses, which return 0), exactly as .NET's DateTime.ParseExact does. It runs client-side for a literal
custom pattern that carries a full date — a 4-digit year (yyyy), a month, and a day-of-month — plus optional time,
using the supported tokens y M d H h m s t with separators and quoted literals; the field widths are strict (MM
wants two digits, "2024-6-15" is rejected), month/day names honour the culture (MMMM/dddd, genitive included), and
a day-name token must agree with the date. A single-char standard specifier ("d", "D"), a 2-digit year
(yy — its value depends on the culture's century window), or a dynamic pattern/culture stays server-side. The
lenient, multi-pattern DateTime.Parse(s) (no explicit pattern) also stays server-side.
Examples#
Format money and a percentage for an explicit culture:
string Price(decimal amount) {
return amount.ToString("C", "sv-SE"); // "1 234,56 kr"
}
string Rate(decimal ratio) {
return ratio.ToString("P1", "en-US"); // "12.5 %"
}Format a date, standard and custom:
string LongDate(DateTime when) {
return when.ToString("D", "de-DE"); // "Freitag, 5. Januar 2024"
}
string Iso(DateTime when) {
return when.ToString("yyyy-MM-dd", "sv-SE");
}Parse a number a user typed in their culture (runs in the browser):
decimal ReadPrice(string entered) {
return Convert.ToDecimal(entered, "sv-SE"); // "1 234,56" → 1234.56, "" → 0
}
int ReadQuantity(string entered) {
return Convert.ToInt(entered, "de-DE"); // "1.234" → 1234, "12,5" → 0 (no decimal point)
}
DateTime ReadDate(string entered) {
return DateTime.ParseExact(entered, "dd/MM/yyyy", "en-GB"); // "25/12/2024"; a mismatch throws
}Set an app-wide default so a culture-less format still localises:
app.DefaultCulture = "sv-SE";
string LocalPrice(decimal amount) {
return amount.ToString("C"); // uses sv-SE → "1 234,56 kr"
}Let each viewer see prices in their own culture — mark a Culture property on the principal [PreferredCulture]:
app.DefaultCulture = "sv-SE";
[Principal] entity Member {
[Required, MaxLength(60)] string Email;
[PreferredCulture] Culture Locale; // a BCP-47 token, e.g. "de-DE"
}
string ViewerPrice(decimal amount) {
return amount.ToString("C"); // renders in the signed-in member's Locale, else sv-SE
}
string ViewerCulture() {
return Session.CurrentCulture; // the effective token: the member's Locale, else "sv-SE"
}See also#
- DateTime — the date/time values these formats render
- Encoding — Base64, URL, HTML — the other C#-faithful text surfaces