在 Windows 上使 ASP.NET 核心服务器 (Kestrel) 区分大小写

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

ASP.NET Linux 容器中的核心应用程序 运行 使用区分大小写的文件系统,这意味着 CSS 和 JS 文件引用必须区分大小写。

但是,Windows文件系统不区分大小写。因此,在开发过程中,您可以使用不正确的大小写引用 CSS 和 JS 文件,但它们仍然可以正常工作。因此,在 Windows 的开发过程中,您不会知道您的应用程序在 Linux 服务器上运行时会崩溃。

是否可以让 Kestrel 在 Windows 上区分大小写,以便我们可以有一致的行为并在上线前找到参考错误?

Windows 7 but not windows 10 中是可能的,据我所知,在 Windows 服务器上根本不可能。

我只能谈论 OS 因为 Kestrel 文档说:

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.

我建议为所有文件名制定一个约定("all lowercase" 通常效果最好)。要检查不一致,您可以 运行 一个使用正则表达式检查大小写错误的简单 PowerShell 脚本。为了方便起见,可以将该脚本安排在时间表中。

我在 ASP.NET Core 中使用中间件修复了这个问题。 我使用的不是标准 app.UseStaticFiles()

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

并将该方法定义为:

/// <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);
}

它还使用:

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);
}

基于@Tracher 的提议和this 博客post,这是一个让物理文件提供程序区分大小写的解决方案,您可以选择强制区分大小写或允许任何大小写而不考虑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;
}

然后在“启动”class 中,确保按如下方式注册内容和 Web 根目录的提供商:

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