Make ASP.NET Core server (Kestrel) case sensitive on Windows Make ASP.NET Core server (Kestrel) case sensitive on Windows asp.net asp.net

Make ASP.NET Core server (Kestrel) case sensitive on Windows


I fixed that using a middleware in ASP.NET Core.Instead of the standard app.UseStaticFiles() I used:

 if (env.IsDevelopment()) app.UseStaticFilesCaseSensitive(); else app.UseStaticFiles();

And defined that method as:

/// <summary>/// Enforces case-correct requests on Windows to make it compatible with Linux./// </summary>public static IApplicationBuilder UseStaticFilesCaseSensitive(this IApplicationBuilder app){    var fileOptions = new StaticFileOptions    {        OnPrepareResponse = x =>        {            if (!x.File.PhysicalPath.AsFile().Exists()) return;            var requested = x.Context.Request.Path.Value;            if (requested.IsEmpty()) return;            var onDisk = x.File.PhysicalPath.AsFile().GetExactFullName().Replace("\\", "/");            if (!onDisk.EndsWith(requested))            {                throw new Exception("The requested file has incorrect casing and will fail on Linux servers." +                    Environment.NewLine + "Requested:" + requested + Environment.NewLine +                    "On disk: " + onDisk.Right(requested.Length));            }        }    };    return app.UseStaticFiles(fileOptions);}

Which also uses:

public static string GetExactFullName(this FileSystemInfo @this){    var path = @this.FullName;    if (!File.Exists(path) && !Directory.Exists(path)) return path;    var asDirectory = new DirectoryInfo(path);    var parent = asDirectory.Parent;    if (parent == null) // Drive:        return asDirectory.Name.ToUpper();    return Path.Combine(parent.GetExactFullName(), parent.GetFileSystemInfos(asDirectory.Name)[0].Name);}


Based on @Tratcher proposal and this blog post, here is a solution to have case aware physical file provider where you can choose to force case sensitivity or allow any casing regardless of OS.

public class CaseAwarePhysicalFileProvider : IFileProvider{    private readonly PhysicalFileProvider _provider;    //holds all of the actual paths to the required files    private static Dictionary<string, string> _paths;    public bool CaseSensitive { get; set; } = false;    public CaseAwarePhysicalFileProvider(string root)    {        _provider = new PhysicalFileProvider(root);        _paths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);    }    public CaseAwarePhysicalFileProvider(string root, ExclusionFilters filters)    {        _provider = new PhysicalFileProvider(root, filters);        _paths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);    }    public IFileInfo GetFileInfo(string subpath)    {        var actualPath = GetActualFilePath(subpath);        if(CaseSensitive && actualPath != subpath) return new NotFoundFileInfo(subpath);        return _provider.GetFileInfo(actualPath);    }    public IDirectoryContents GetDirectoryContents(string subpath)    {        var actualPath = GetActualFilePath(subpath);        if(CaseSensitive && actualPath != subpath) return NotFoundDirectoryContents.Singleton;        return _provider.GetDirectoryContents(actualPath);    }    public IChangeToken Watch(string filter) => _provider.Watch(filter);    // Determines (and caches) the actual path for a file    private string GetActualFilePath(string path)    {        // Check if this has already been matched before        if (_paths.ContainsKey(path)) return _paths[path];        // Break apart the path and get the root folder to work from        var currPath = _provider.Root;        var segments = path.Split(new [] { '/' }, StringSplitOptions.RemoveEmptyEntries);        // Start stepping up the folders to replace with the correct cased folder name        for (var i = 0; i < segments.Length; i++)        {            var part = segments[i];            var last = i == segments.Length - 1;            // Ignore the root            if (part.Equals("~")) continue;            // Process the file name if this is the last segment            part = last ? GetFileName(part, currPath) : GetDirectoryName(part, currPath);            // If no matches were found, just return the original string            if (part == null) return path;            // Update the actualPath with the correct name casing            currPath = Path.Combine(currPath, part);            segments[i] = part;        }        // Save this path for later use        var actualPath = string.Join(Path.DirectorySeparatorChar, segments);        _paths.Add(path, actualPath);        return actualPath;    }    // Searches for a matching file name in the current directory regardless of case    private static string GetFileName(string part, string folder) =>        new DirectoryInfo(folder).GetFiles().FirstOrDefault(file => file.Name.Equals(part, StringComparison.OrdinalIgnoreCase))?.Name;    // Searches for a matching folder in the current directory regardless of case    private static string GetDirectoryName(string part, string folder) =>        new DirectoryInfo(folder).GetDirectories().FirstOrDefault(dir => dir.Name.Equals(part, StringComparison.OrdinalIgnoreCase))?.Name;}

Then in Startup class, make sure you register a provider for content and web root as follow:

        _environment.ContentRootFileProvider = new CaseAwarePhysicalFileProvider(_environment.ContentRootPath);        _environment.WebRootFileProvider = new CaseAwarePhysicalFileProvider(_environment.WebRootPath);


It was possible in Windows 7 but not windows 10 and as far as I can tell, it's also not possible on Windows Server at all.

I can only talk about the OS because the Kestrel documentation says:

The URLs for content exposed with UseDirectoryBrowser and UseStaticFiles are subject to the case sensitivity and character restrictions of the underlying file system. For example, Windows is case insensitive—macOS and Linux aren't.

I'd recommend a convention for all filenames ("all lowercase" usually works best). And to check for inconsistencies, you can run a simple PowerShell script that uses regular expressions to check for wrong casing. And that script can be put on a schedule for convenience.