Conversion · sanitising · matching

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.

✓ Real output, not mock-ups ✓ No runtime reflection ✓ .NET 8, 9, and 10
input://untrusted real output
IN whatever the user 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)
3HTML policies, from strip-everything to safe links
20payloads in the explorer below, all run through the real library
4semantic match modes: phone, email, alphanumeric, path
0exceptions thrown by ConvertTo anywhere in the tables below
Conversion

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.

Inputint.ParseConvertTo<int>(-1)TryConvertTo<int>
"42"4242true, 42
" 42 "4242true, 42
"-7"-7-7true, -7
"1,234"throws FormatException-1false
"12.7"throws FormatException-1false
"abc"throws FormatException-1false
""throws FormatException-1false
"2147483648"throws OverflowException-1false
nullthrows ArgumentNullException-1false

Booleans

Yes, Y, 1, T and their opposites are understood. Anything else falls back, so you choose the default.

Inputbool.ParseConvertTo<bool>(false)ConvertTo<bool>(true)
"true"TrueTrueTrue
"yes"throws FormatExceptionTrueTrue
"Y"throws FormatExceptionTrueTrue
"1"throws FormatExceptionTrueTrue
"no"throws FormatExceptionFalseFalse
"0"throws FormatExceptionFalseFalse
"maybe"throws FormatExceptionFalseTrue

Decimals

Exponents, separators and padding are accepted.

Inputdouble.ParseConvertTo<double>(-1)
"3.14"3.143.14
"1e3"10001000
"1,234.5"1234.51234.5
" 7 "77
"abc"throws FormatException-1

Dates

An ambiguous date is ambiguous: it follows the current culture. ISO dates are always safe.

InputDateTime.Parse (invariant)ConvertTo<DateTime> en-GBConvertTo<DateTime> en-US
"2026-10-11"2026-10-112026-10-112026-10-11
"03/04/2026"2026-03-042026-04-032026-03-04
"31/12/2026"throws FormatException2026-12-311900-01-01
"not a date"throws FormatException1900-01-011900-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
HTML sanitising

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.

no tags

PermitInlineFormatting

Keeps lightweight emphasis and nothing interactive. For comments and messages.

<b> <i> <u> <em> <strong>

PermitLinks

Adds anchors, but only with a safe href. Use it only when links are genuinely needed.

+ <a href>

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.

INPUT
StripAll
PermitInlineFormatting
PermitLinks
string safe = comment.SanitizeHtml(HtmlSanitizationPolicy.PermitLinks); string preview = html.TruncateSemantic(140);

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.

Truncation

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.

Limit 20 characters
Input<p>Fish &amp; <b>chips</b> are <em>great</em> with vinegar</p>
Substring<p>Fish &amp; <b>chi
TruncateSemantic<p>Fish &amp; <b>chips</b> are...</p>
Limit 12 characters
InputTom &amp; Jerry &copy; 2026 forever
SubstringTom &amp; Je
TruncateSemanticTom &amp; Jerry...
Limit 24 characters
Input<p>Read the <a href="https://example.com/docs">documentation</a> before you deploy.</p>
Substring<p>Read the <a href="htt
TruncateSemantic<p>Read the <a href="https://example.com/docs">documentation</a>...</p>
Semantic matching

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.

ModeLeftRightMatch
PhoneNumber(555) 123-4567555.123.4567true
PhoneNumber+1 (555) 111-222215551112222true
PhoneNumber+44 20 7946 0958020 7946 0958false
Email User@Example.COM user@example.comtrue
Emaila@example.comb@example.comfalse
AlphaNumericOnlyABC-123abc 123true
NormalizedPathC:\Temp\Logs\c:/temp/logstrue
Small things that bite

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}");
PropertyOldNew
Age3637
CityLondonCambridge

Pluralize

WordCount 1Count 2
childchildchildren
personpersonpeople
goosegoosegeese
boxboxboxes
citycitycities
knifeknifeknives

The Token property differed too, but [SkipDelta] kept it out of the result.

Transformations

Install once.
Trust less input.

NuGet — the everyday toolkit
$ dotnet add package Transformations
View on NuGet.org .NET 8 · .NET 9 · .NET 10 · MIT licensed