在Linux容器中运行的ASP.NET核心应用程序使用区分大小写的文件系统,这意味着CSS和JS文件引用必须是大小写正确的。
但是,Windows文件系统不区分大小写。因此,在开发过程中,您可以使用错误的大小写引用CSS和JS文件,但它们可以正常工作。所以在Windows开发过程中你不会知道你的应用程序在Linux服务器上运行时会破坏。
反正有没有让Windows上的Kestrel区分大小写,以便我们可以在上线之前获得一致的行为并找到参考错误?
我修复了使用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);
}
它可能在Windows 7而不是windows 10,据我所知,它在Windows Server上根本不可能。
我只能谈论操作系统,因为Kestrel文档说:
使用
UseDirectoryBrowser
和UseStaticFiles
公开的内容的URL受基础文件系统的区分大小写和字符限制的约束。例如,Windows不区分大小写 - macOS而Linux则不区分大小写。
我建议所有文件名的约定(“全小写”通常效果最好)。要检查是否存在不一致,可以运行一个简单的PowerShell脚本,该脚本使用正则表达式来检查错误的外壳。为方便起见,该脚本可以按计划进行。
根据@Tratcher提案和this博客文章,这里有一个案例感知物理文件提供程序的解决方案,您可以选择强制区分大小写或允许任何外壳,无论操作系统如何。
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;
}
然后在Startup类中,确保为内容和Web root注册提供程序,如下所示:
_environment.ContentRootFileProvider = new CaseAwarePhysicalFileProvider(_environment.ContentRootPath);
_environment.WebRootFileProvider = new CaseAwarePhysicalFileProvider(_environment.WebRootPath);