
Blazor WebAssembly is a fantastic framework for building rich client-side applications, almost entirely in C#. However, it comes with a few subtle quirks that can catch us developers off guard. One of the less obvious quirks to be aware of surfaces when you need to validate or sanitise a filename specified by a user; something that seems straightforward until you realise that the runtime environment changes the rules beneath your feet.
Long story short, the list of invalid filename characters returned by .NET varies depending on the platform your code is running on, and Blazor WebAssembly is somewhat unusual in that it runs within a WebAssembly Sandbox, as opposed to running directly on a Windows or Linux machine.
In this post, I’ll walk through the problem and explain why the Path.GetInvalidFileNameChars method behaves differently in a Blazor WebAssembly context, explore the surprising edge cases that Windows file systems introduce, and then build a robust FileNameSanitizer utility class that handles all the quirks correctly.
Path.GetInvalidFileNameChars in Blazor WebAssembly
For a standard .NET application running on a Windows device, the Path.GetInvalidFileNameChars method returns a comprehensive list of characters that are forbidden in filenames. This includes characters such as \, /, :, *, ?, ", <, >, and |, along with a range of control characters.
However, in a Blazor WebAssembly application, the story is different. Because the app runs within the browser on the .NET WebAssembly runtime, the host operating system is effectively Linux (or, more precisely, a Unix-like environment as far as .NET is concerned). This means that the Path.GetInvalidFileNameChars method only returns the set of filename characters that are invalid on Linux, which is very permissive; only the null (\0) and forward slash (/) characters are considered invalid.
This has real consequences for production code; if you rely on Path.GetInvalidFileNameChars alone inside a Blazor WebAssembly app, you will unwittingly be allowing users to specify filenames that contain characters like :, *, or ?. Those filenames will look perfectly fine in the browser, but cannot be safely downloaded to a Windows machine or attached to an email, for example. Although browsers and email systems usually have built-in mechanisms to prevent things from breaking, the behaviour will vary across systems and cannot be relied upon.
The fix in principle is straightforward: define the full set of invalid Windows filename characters explicitly, rather than relying on the runtime to provide them.
Windows Reserved Filenames
Here’s a quirk worth knowing about… Beyond the individual characters that are considered invalid, Windows features an unusual restriction that catches many developers by surprise; a set of reserved device names that cannot be used as filenames, regardless of extension.
The reserved names are as follows.
AUX,CON,ÂNUL,PRNCOM1throughCOM9LPT1throughLPT9
The check applied by Windows against the above list is case-insensitive and applies to the base name before the extension. As an example, a file called CON.txt is just as invalid on Windows as one called con.txt, Con.TXT, or CON.
This restriction dates back to the days of MS-DOS, where these names referred to system devices (auxiliary port, console, null device, printer, etc.). Modern versions of Windows continue to honour them today for backward compatibility.
If you ever try to create a file with one of these names using File Explorer on Windows, you will receive a cryptic error message, usually ‘The specified device name is invalid’.
Setup
Before we get into the code, please note the assumed environment setup before proceeding.
The code from this post assumes you are targeting .NET 8 or later within a standard Blazor Web App.
Aside from having an IDE such as Visual Studio or Visual Studio Code installed on your machine, all you need to do is start with an empty Blazor Web App with the ‘Interactive render mode’ set to ‘WebAssembly’ and then add the code presented in the sections below to follow along.
The FileNameSanitizer Class
Let’s build a sanitiser class to handle the nuances of filename sanitisation and validation.
The goals are as follows.
- Replace any filename character that is invalid on Windows (not just the Linux subset that is returned by the
Path.GetInvalidFileNameCharsmethod in WASM). - Strip leading/trailing whitespace and trailing dots, which Windows also disallows.
- Detect and handle Windows reserved device names (
AUX,CON, etc.). - Fall back gracefully to a safe default filename when the input is empty or collapses entirely after sanitisation.
- Enforce a configurable maximum filename length, while preserving the file extension where possible.
The full code for the FileNameSanitizer class has been included below for reference.
using System.Text; /// <summary> /// Sanitises filenames to ensure they are valid on both Windows and Linux filesystems. /// </summary> public static class FileNameSanitizer {     /// <summary>     /// The standard maximum filename length on Windows and Linux is 255 characters.     /// </summary>     private const int MaxFileNameLength = 255;     // The full set of characters that are invalid on Windows.     // We define these explicitly because Path.GetInvalidFileNameChars()     // returns only the Linux subset when running in Blazor WebAssembly.     private static readonly HashSet<char> WindowsInvalidChars = new()     {         '\"', '<', '>', '|', '\0',         (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,         (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,         (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,         (char)31, ':', '*', '?', '\\', '/'     };     // Windows device names that are reserved regardless of file extension.    private static readonly HashSet<string> WindowsReservedNames = new(StringComparer.OrdinalIgnoreCase)     {         "AUX", "CON", "NUL", "PRN",         "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",         "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"     };     /// <summary>     /// Sanitises the specified filename such that it is valid on both Windows and Linux.     /// </summary>     /// <param name="input">The raw filename to sanitise.</param>     /// <param name="replacement">The string used to replace invalid characters. Defaults to "_".</param>     /// <param name="maxLength">The maximum permitted filename length. Defaults to 255.</param>     /// <returns>A sanitised filename string.</returns>     public static string Sanitize(string input, string replacement = "_", int maxLength = MaxFileNameLength)     {         const string untitled = "untitled";         if (string.IsNullOrWhiteSpace(input))         {             return untitled;         }         var sb = new StringBuilder(input.Length);         // 1. Replace every character that is invalid on Windows.         foreach (var c in input)         {             if (WindowsInvalidChars.Contains(c))             {                 sb.Append(replacement);             }             else             {                 sb.Append(c);             }         }         var result = sb.ToString();         // 2. Trim leading/trailing whitespace and trailing dots.         //    Windows silently strips trailing dots and spaces from filenames,         //    which leads to confusing mismatches between the name displayed         //    and the name actually stored on disk.         result = result.Trim().TrimEnd('.', ' ');         // 3. Prefix reserved Windows device names.         //    The check is against the base name only - CON.txt is still invalid.         var nameWithoutExtension = Path.GetFileNameWithoutExtension(result);         if (WindowsReservedNames.Contains(nameWithoutExtension))         {             result = "_" + result;         }         // 4. Guard against the result being empty after all the above transformations.         if (string.IsNullOrWhiteSpace(result))         {             result = untitled;         }         // 5. Enforce the maximum length, preserving the extension where possible.         if (result.Length > maxLength)         {             var ext = Path.GetExtension(result);             var name = Path.GetFileNameWithoutExtension(result);             var allowedNameLength = maxLength - ext.Length;             result = allowedNameLength < 1                 ? result[..maxLength]                 : name[..allowedNameLength] + ext;         }         return result;     }     /// <summary>     /// Returns true if the specified filename is valid on both Windows and Linux.     /// </summary>     /// <param name="input">The filename to validate.</param>     /// <param name="maxLength">The maximum permitted filename length. Defaults to 255.</param>     public static bool IsValid(string input, int maxLength = MaxFileNameLength)     {         if (string.IsNullOrWhiteSpace(input))         {             return false;         }         if (input.Length > maxLength)         {             return false;         }         if (input.Any(c => WindowsInvalidChars.Contains(c)))         {             return false;         }         if (input != input.Trim() || input != input.TrimEnd('.', ' '))         {             return false;         }         var nameWithoutExtension = Path.GetFileNameWithoutExtension(input);         if (WindowsReservedNames.Contains(nameWithoutExtension))         {             return false;         }         return true;     } }
Logic Walkthrough
Let’s walk through each step of the Sanitize method logic.
Step 1: Replace Invalid Characters
In step 1, the code iterates over every character in the input and replaces any that are contained within WindowsInvalidChars with the configured replacement string. A HashSet<char> is used for the collection of invalid characters, which keeps each lookup at O(1).
The replacement character defaults to an underscore (_). You could pass an empty string if you prefer to strip invalid characters entirely rather than substitute them. However, the underscore is a good standard and is the default replacement character used by Chromium browsers such as Edge when handling invalid filename characters.
Step 2: Trim Whitespace and Trailing Dots
Windows silently truncates trailing spaces and dots from filenames at the filesystem level. This creates a subtle trap where a file saved as report .pdf is actually stored as report.pdf, but subsequent code treating the two as distinct names will fail. In step 2, we trim both proactively to eliminate any ambiguity.
Step 3: Handle Reserved Device Names
The reserved device name check is applied to the base name only, using the Path.GetFileNameWithoutExtension method. This correctly catches CON.txt, NUL.csv, COM1.log, and so on. When a match is found, the method prefixes the result with an underscore character, producing _CON.txt for example.
Step 4: Fallback to “untitled”
It is possible for a user-specified filename to consist entirely of invalid characters, leaving nothing behind after replacements are made. The guard here prevents an empty string from being returned by the method.
Step 5: Enforce Maximum Length
When truncation is necessary due to the maximum filename length being exceeded, the method attempts to preserve the file extension. If the extension itself is longer than maxLength (an unusual but possible edge case), the method falls back to a hard truncation of the full string.
Validation
The IsValid method performs all the key validations needed to ensure that a filename is well-formed and enforces the following set of rules for the filename.
- Must not be null, empty, or whitespace.
- Must not exceed the maximum length.
- Must not contain any characters considered invalid on Windows (implicitly covers Linux).
- Must not have any trailing dots.
- Must not clash with Windows reserved device names.
If all the above conditions are met, the method with return true, otherwise, it will return false.
Blazor Usage Example
Below is a minimal example of using both methods from the FileNameSanitizer class inside a Blazor page component.
@page "/upload" <h3>Upload a File</h3> <InputText @bind-Value="FileName" placeholder="Enter a filename..." /> @if (!string.IsNullOrWhiteSpace(FileName)) {     @if (FileNameSanitizer.IsValid(FileName))     {         <p class="text-success">Filename is valid.</p>     }     else     {         <p class="text-warning">             Filename contains invalid characters or is reserved.             Suggested alternative: <strong>@FileNameSanitizer.Sanitize(FileName)</strong>         </p>     } } @code {     private string FileName { get; set; } = string.Empty; }
The IsValid method is used to control the validation message, and the Sanitize method provides a helpful suggestion for an alternative filename that the user can use instead.
Testing the Edge Cases
Before wrapping up, it’s worth running through a handful of examples to confirm that the sanitisation method is operating according to what you would expect.
| Input | Output | Reason |
|---|---|---|
report.pdf |
report.pdf |
Already valid |
my:file?.txt |
my_file_.txt |
: and ? are Windows-invalid |
CON.txt |
_CON.txt |
Reserved device name |
NUL |
_NUL |
Reserved device name, no extension |
... |
untitled |
Collapses to empty after dot trimming |
 report  |
report |
Leading/trailing whitespace trimmed |
report. |
report |
Trailing dot trimmed |
When passing the above filenames to the IsValid method, only the first one should meet the validation requirements.
Summary
The reliance of Blazor WebAssembly on the Linux runtime means that we cannot trust the built-in Path.GetInvalidFileNameChars method to provide a Windows-safe exclusion list.
By defining the full set of invalid Windows filename characters explicitly, combining this with reserved name detection and whitespace trimming, and wrapping everything in a utility class, we end up with a clean way to integrate filename sanitation and validation into any Blazor component that accepts user-specified filenames.
The solution can be extended further by surfacing more specific validation messages e.g. distinguishing between an invalid character, a reserved name, or an exceeded length. The class could also be made injectable as a service if it is needed across many components. For most use cases, however, the static utility approach shown above is perfectly sufficient.


Comments