Feed it garbage.
Get a decision.
Strings from forms, files and APIs are never what the type says they are. Transformations turns them into values with a fallback instead of an exception, strips the dangerous HTML, cuts text without breaking markup, and compares what people mean rather than what they typed.
"1,234"-1ConvertTo<int>(-1)"maybe"falseConvertTo<bool>(false)<img src=x onerror=alert(1)>(empty)SanitizeHtml(PermitLinks)<a href="javascript:alert(1)">click</a><a>click</a>SanitizeHtml(PermitLinks)" User@Example.COM "true== "user@example.com" (Email)A fallback, not a crash.
ConvertTo<T>(default) and TryConvertTo<T> never throw on bad
input. You choose what “couldn’t read it” means. Every cell below was produced by
running the library, next to what the plain .NET parser does.
Integers
Padding is fine. Everything else that is not an integer takes your default.
| Input | int.Parse | ConvertTo<int>(-1) | TryConvertTo<int> |
|---|---|---|---|
"42" | 42 | 42 | true, 42 |
" 42 " | 42 | 42 | true, 42 |
"-7" | -7 | -7 | true, -7 |
"1,234" | throws FormatException | -1 | false |
"12.7" | throws FormatException | -1 | false |
"abc" | throws FormatException | -1 | false |
"" | throws FormatException | -1 | false |
"2147483648" | throws OverflowException | -1 | false |
null | throws ArgumentNullException | -1 | false |
Booleans
Yes, Y, 1, T and their opposites are understood. Anything else falls back, so you choose the default.
| Input | bool.Parse | ConvertTo<bool>(false) | ConvertTo<bool>(true) |
|---|---|---|---|
"true" | True | True | True |
"yes" | throws FormatException | True | True |
"Y" | throws FormatException | True | True |
"1" | throws FormatException | True | True |
"no" | throws FormatException | False | False |
"0" | throws FormatException | False | False |
"maybe" | throws FormatException | False | True |
Decimals
Exponents, separators and padding are accepted.
| Input | double.Parse | ConvertTo<double>(-1) |
|---|---|---|
"3.14" | 3.14 | 3.14 |
"1e3" | 1000 | 1000 |
"1,234.5" | 1234.5 | 1234.5 |
" 7 " | 7 | 7 |
"abc" | throws FormatException | -1 |
Dates
An ambiguous date is ambiguous: it follows the current culture. ISO dates are always safe.
| Input | DateTime.Parse (invariant) | ConvertTo<DateTime> en-GB | ConvertTo<DateTime> en-US |
|---|---|---|---|
"2026-10-11" | 2026-10-11 | 2026-10-11 | 2026-10-11 |
"03/04/2026" | 2026-03-04 | 2026-04-03 | 2026-03-04 |
"31/12/2026" | throws FormatException | 2026-12-31 | 1900-01-01 |
"not a date" | throws FormatException | 1900-01-01 | 1900-01-01 |
Read the table honestly. Integers do not accept thousands separators
("1,234" takes the default) while decimals do, which follows the .NET parsers
underneath. And a default hides the failure: when you need to know whether parsing worked,
use TryConvertTo. Results above were produced under the invariant culture, except
the date columns, which name their culture.
int qty = form["qty"].ConvertTo<int>(1); // "abc" -> 1 bool optIn = form["optin"].ConvertTo<bool>(false); // "yes" -> true if (input.TryConvertTo<int>(out var n)) Process(n); // only when it really parsed
Pick how much HTML you trust.
Three policies, from “none at all” to “emphasis and safe links”. Script and
style blocks go in every policy, every on* attribute is removed, and unsafe link
targets are dropped.
StripAll
Removes every tag. For logs, notifications, exports and search indexing. The smallest attack surface.
PermitInlineFormatting
Keeps lightweight emphasis and nothing interactive. For comments and messages.
PermitLinks
Adds anchors, but only with a safe href. Use it only when links are genuinely needed.
Payload explorer
Pick a payload to see the real result under each policy. Nothing here is rendered as HTML: it is all shown as text.
Sanitising is one layer, not the whole defence. Keep it near the input boundary, still
encode for the place you output to, validate on the server, and use a Content Security Policy.
Note that an unclosed <script> leaves its inner text behind as harmless plain text.
Cut the text, keep the markup.
Substring will happily slice through a tag, an attribute or an entity and hand you
broken HTML. TruncateSemantic cuts on a word boundary, never inside an entity, and closes
only the tags that are still open.
<p>Fish & <b>chips</b> are <em>great</em> with vinegar</p><p>Fish & <b>chi<p>Fish & <b>chips</b> are...</p>Tom & Jerry © 2026 foreverTom & JeTom & Jerry...<p>Read the <a href="https://example.com/docs">documentation</a> before you deploy.</p><p>Read the <a href="htt<p>Read the <a href="https://example.com/docs">documentation</a>...</p>Compare what people mean.
IsSemanticMatch normalises before it compares, so formatting differences stop
mattering. The last phone row shows the limit on purpose: it compares digits and does not know about country codes.
| Mode | Left | Right | Match |
|---|---|---|---|
| PhoneNumber | (555) 123-4567 | 555.123.4567 | true |
| PhoneNumber | +1 (555) 111-2222 | 15551112222 | true |
| PhoneNumber | +44 20 7946 0958 | 020 7946 0958 | false |
User@Example.COM | user@example.com | true | |
a@example.com | b@example.com | false | |
| AlphaNumericOnly | ABC-123 | abc 123 | true |
| NormalizedPath | C:\Temp\Logs\ | c:/temp/logs | true |
Changes and plurals, without the boilerplate.
ObjectDelta reports which properties changed between two objects, ready for an audit
log, and skips the ones you mark. Pluralize handles the irregular words a simple
“add an s” gets wrong.
class Person { public string Name { get; set; } public int Age { get; set; } public string City { get; set; } [SkipDelta] public string Token { get; set; } } var changes = ObjectDelta.Compare(before, after); foreach (var c in changes) log.Info($"{c.PropertyName}: {c.OldValue} -> {c.NewValue}");
| Property | Old | New |
|---|---|---|
Age | 36 | 37 |
City | London | Cambridge |
Pluralize
| Word | Count 1 | Count 2 |
|---|---|---|
child | child | children |
person | person | people |
goose | goose | geese |
box | box | boxes |
city | city | cities |
knife | knife | knives |
The Token property differed too, but [SkipDelta] kept it out of the result.