理解ASP.NET Core - 文件服务器(File Server)
注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录
提供静态文件
静态文件默认存放在 Web根目录(Web Root) 中,路径为 项目根目录(Content Root) 下的wwwroot
文件夹,也就是{Content Root}/wwwroot
。
如果你调用了Host.CreateDefaultBuilder
方法,那么在该方法中,会通过UseContentRoot
方法,将程序当前工作目录(Directory.GetCurrentDirectory()
)设置为项目根目录。具体可以查看主机一节。
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
当然,你也可以通过UseWebRoot
扩展方法将默认的路径{Content Root}/wwwroot
修改为自定义目录(不过,你改它干啥捏?)
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
// 配置静态资源的根目录为 mywwwroot, 默认为 wwwroot
webBuilder.UseWebRoot("mywwwroot");
webBuilder.UseStartup<Startup>();
});
为了方便,后面均使用 wwwroot 来表示Web根目录
首先,我们先在 wwwroot 文件夹下创建一个名为 config.json 的文件,内容随便填写
注意,确保 wwwroot 下的文件的属性为“如果较新则复制”或“始终复制”。
接着,我们通过UseStaticFiles
扩展方法,来注册静态文件中间件StaticFileMiddleware
:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseStaticFiles();
}
现在,尝试一下通过 http://localhost:5000/config.json 来获取 wwwroot/config.json 的文件内容吧
如果你的项目中启用SwaggerUI,那么你会发现,即使你没有手动通过调用
UseStaticFiles()
添加中间件,你也可以访问 wwwroot 文件下的文件,这是因为 SwaggerUIMiddleware 中使用了 StaticFileMiddleware
提供Web根目录之外的文件
上面我们已经能够提供 wwwroot 文件夹内的静态文件了,那如果我们的文件不在 wwwroot 文件夹内,那如何提供呢?
很简单,我们可以针对StaticFileMiddleware
中间件进行一些额外的配置,了解一下配置项:
public abstract class SharedOptionsBase
{
// 用于自定义静态文件的相对请求路径
public PathString RequestPath { get; set; }
// 文件提供程序
public IFileProvider FileProvider { get; set; }
// 是否补全路径末尾斜杠“/”,并重定向
public bool RedirectToAppendTrailingSlash { get; set; }
}
public class StaticFileOptions : SharedOptionsBase
{
// ContentType提供程序
public IContentTypeProvider ContentTypeProvider { get; set; }
// 如果 ContentTypeProvider 无法识别文件类型,是否仍作为默认文件类型提供
public bool ServeUnknownFileTypes { get; set; }
// 当 ServeUnknownFileTypes = true 时,若出现无法识别的文件类型,则将该属性的值作为此文件的类型
// 当 ServeUnknownFileTypes = true 时,必须赋值该属性,才会生效
public string DefaultContentType { get; set; }
// 当注册了HTTP响应压缩中间件时,是否对文件进行压缩
public HttpsCompressionMode HttpsCompression { get; set; } = HttpsCompressionMode.Compress;
// 在HTTP响应的 Status Code 和 Headers 设置完毕之后,Body 写入之前进行调用
// 用于添加或更改 Headers
public Action<StaticFileResponseContext> OnPrepareResponse { get; set; }
}
假设我们现在有这样一个文件目录结构:
- wwwroot
- config.json
- files
- file.json
然后,除了用于提供 wwwroot 静态文件的中间件外,我们还要注册一个用于提供 files 静态文件的中间件:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// 提供 wwwroot 静态文件
app.UseStaticFiles();
// 提供 files 静态文件
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files")),
// 指定文件的访问路径,允许与 FileProvider 中的文件夹不同名
// 如果不指定,则可通过 http://localhost:5000/file.json 获取,
// 如果指定,则需要通过 http://localhost:5000/files/file.json 获取
RequestPath = "/files",
OnPrepareResponse = ctx =>
{
// 配置前端缓存 600s(为了后续示例的良好运行,建议先不要配置该Header)
ctx.Context.Response.Headers.Add(HeaderNames.CacheControl, "public,max-age=600");
}
});
}
建议将公开访问的文件放置到 wwwroot 目录下,而将需要授权访问的文件放置到其他目录下(在调用
UseAuthorization
之后调用UseStaticFiles
并指定文件目录)
提供目录浏览
上面,我们可以通过Url访问某一个文件的内容,而通过UseDirectoryBrowser
,注册DirectoryBrowserMiddleware
中间件,可以让我们在浏览器中以目录的形式来访问文件列表。
另外,DirectoryBrowserMiddleware
中间件的可配置项除了SharedOptionsBase
中的之外,还有一个Formatter
,用于自定义目录视图。
public class DirectoryBrowserOptions : SharedOptionsBase
{
public IDirectoryFormatter Formatter { get; set; }
}
示例如下:
public void ConfigureServices(IServiceCollection services)
{
services.AddDirectoryBrowser();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// 通过 http://localhost:5000,即可访问 wwwroot 目录
app.UseDirectoryBrowser();
// 通过 http://localhost:5000/files,即可访问 files 目录
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
// 如果指定了没有在 UseStaticFiles 中提供的文件目录,虽然可以浏览文件列表,但是无法访问文件内容
FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files")),
// 这里一定要和 StaticFileOptions 中的 RequestPath 一致,否则会无法访问文件
RequestPath = "/files"
});
}
提供默认页
通过UseDefaultFiles
,注册DefaultFilesMiddleware
中间件,允许在访问静态文件、但未提供文件名的情况下(即传入的是一个目录的路径),提供默认页的展示。
注意:
UseDefaultFiles
必须在UseStaticFiles
之前进行调用。因为DefaultFilesMiddleware
仅仅负责重写Url,实际上默认页文件,仍然是通过StaticFilesMiddleware
来提供的。
默认情况下,该中间件会按照顺序搜索文件目录下的HTML页面文件:
- default.htm
- default.html
- index.htm
- index.html
另外,DefaultFilesMiddleware
中间件的可配置项除了SharedOptionsBase
中的之外,还有一个DefaultFileNames
,是个列表,用于自定义默认页的文件名,里面的默认值就是上面提到的4个文件名。
public class DefaultFilesOptions : SharedOptionsBase
{
public IList<string> DefaultFileNames { get; set; }
}
示例如下:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// 会去 wwwroot 寻找 default.htm 、default.html 、index.htm 或 index.html 文件作为默认页
app.UseDefaultFiles();
// 设置 files 目录的默认页
var defaultFilesOptions = new DefaultFilesOptions();
defaultFilesOptions.DefaultFileNames.Clear();
// 指定默认页名称
defaultFilesOptions.DefaultFileNames.Add("index1.html");
// 指定请求路径
defaultFilesOptions.RequestPath = "/files";
// 指定默认页所在的目录
defaultFilesOptions.FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files"));
app.UseDefaultFiles(defaultFilesOptions);
}
UseFileServer
UseFileServer
集成了UseStaticFiles
、UseDefaultFiles
和UseDirectoryBrowser
的功能,用起来方便一些,也是我们项目中使用的首选扩展方法。
先看一下FileServerOptions
:
public class FileServerOptions : SharedOptionsBase
{
public FileServerOptions()
: base(new SharedOptions())
{
StaticFileOptions = new StaticFileOptions(SharedOptions);
DirectoryBrowserOptions = new DirectoryBrowserOptions(SharedOptions);
DefaultFilesOptions = new DefaultFilesOptions(SharedOptions);
EnableDefaultFiles = true;
}
public StaticFileOptions StaticFileOptions { get; private set; }
public DirectoryBrowserOptions DirectoryBrowserOptions { get; private set; }
public DefaultFilesOptions DefaultFilesOptions { get; private set; }
// 默认禁用目录浏览
public bool EnableDirectoryBrowsing { get; set; }
// 默认启用默认页(在构造函数中初始化的)
public bool EnableDefaultFiles { get; set; }
}
可以看到,FileServerOptions
包含了StaticFileOptions
、DirectoryBrowserOptions
和DefaultFilesOptions
三个选项,可以针对StaticFileMiddleware
、DirectoryBrowserMiddleware
和DefaultFilesMiddleware
进行自定义配置。另外,其默认启用了静态文件和默认页,禁用了目录浏览。
下面举个例子熟悉一下:
假设文件目录:
- files
- images
- 1.jpg
- file.json
- myindex.html
- images
public void ConfigureServices(IServiceCollection services)
{
// 如果将 EnableDirectoryBrowsing 设为 true,记得注册服务
services.AddDirectoryBrowser();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// 启用 StaticFileMiddleware
// 启用 DefaultFilesMiddleware
// 禁用 DirectoryBrowserMiddleware
// 默认指向 wwwroot
app.UseFileServer();
// 针对 files 文件夹配置
var fileServerOptions = new FileServerOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files")),
RequestPath = "/files",
EnableDirectoryBrowsing = true
};
fileServerOptions.StaticFileOptions.OnPrepareResponse = ctx =>
{
// 配置缓存600s
ctx.Context.Response.Headers.Add(HeaderNames.CacheControl, "public,max-age=600");
};
fileServerOptions.DefaultFilesOptions.DefaultFileNames.Clear();
fileServerOptions.DefaultFilesOptions.DefaultFileNames.Add("myindex.html");
app.UseFileServer(fileServerOptions);
}
当访问 http://localhost:5000/files 时,由于在DefaultFilesOptions.DefaultFileNames
中添加了文件名myindex.html
,所以可以找到默认页,此时会显示默认页的内容。
假如我们没有在DefaultFilesOptions.DefaultFileNames
中添加文件名myindex.html
,那么便找不到默认页,但由于启用了DirectoryBrowsing
,所以此时会展示文件列表。
核心配置项
FileProvider
上面我们已经见过PhysicalFileProvider
了,它仅仅是众多文件提供程序中的一种。所有的文件提供程序均实现了IFileProvider
接口:
public interface IFileProvider
{
// 获取给定路径的目录信息,可枚举该目录中的所有文件
IDirectoryContents GetDirectoryContents(string subpath);
// 获取给定路径的文件信息
IFileInfo GetFileInfo(string subpath);
// 创建指定 filter 的 ChangeToken
IChangeToken Watch(string filter);
}
public interface IDirectoryContents : IEnumerable<IFileInfo>, IEnumerable
{
bool Exists { get; }
}
public interface IFileInfo
{
bool Exists { get; }
bool IsDirectory { get; }
DateTimeOffset LastModified { get; }
// 字节(bytes)长度
// 如果是目录或文件不存在,则是 -1
long Length { get; }
// 目录或文件名,纯文件名,不包括路径
string Name { get; }
// 文件路径,包含文件名
// 如果文件无法直接访问,则返回 null
string PhysicalPath { get; }
// 创建该文件只读流
Stream CreateReadStream();
}
常用的文件提供程序有以下三种:
- PhysicalFileProvider
- ManifestEmbeddedFileProvider
- CompositeFileProvider
glob模式
在介绍这三种文件提供程序之前,先说一下glob模式
,即通配符模式
。两个通配符分别是*
和**
。
*
:匹配当前目录层级(不包含子目录)下的任何内容、任何文件名或任何文件扩展名,可以通过/
、\
和.
进行分隔。**
:匹配目录多层级(包含子目录)的任何内容,用于递归匹配多层级目录的多个文件。
PhysicalFileProvider
PhysicalFileProvider
用于提供物理文件系统的访问。该提供程序需要将文件路径范围限定在一个目录及其子目录中,不能访问目录外部的内容。
当实例化该文件提供程序时,需要提供一个绝对的目录路径,作为文件目录的root。
PhysicalFileProvider
目录或文件路径不支持glob(通配符)模式。
ManifestEmbeddedFileProvider
ManifestEmbeddedFileProvider
用于提供嵌入在程序集中的文件的访问。
可能你对这个嵌入文件比较陌生,没关系,请按照下面的步骤来:
- 安装Nuget包:
Install-Package Microsoft.Extensions.FileProviders.Embedded
- 编辑
.csproj
文件:- 添加
<GenerateEmbeddedFilesManifest>
,并设置为true
- 使用
<EmbeddedResource>
添加要嵌入的文件
- 添加
以下是 .csproj 文件的示例:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="5.0.11" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="files\**" />
</ItemGroup>
</Project>
现在我们通过ManifestEmbeddedFileProvider
来提供嵌入到程序集的 files 目录下文件的访问:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
var fileServerOptions = new FileServerOptions();
fileServerOptions.StaticFileOptions.FileProvider = new ManifestEmbeddedFileProvider(Assembly.GetExecutingAssembly(), "/files");
fileServerOptions.StaticFileOptions.RequestPath = "/files";
app.UseFileServer(fileServerOptions);
}
现在,你可以通过 http://localhost:5000/files/file.json 来访问文件了。
CompositeFileProvider
CompositeFileProvider
用于将多种文件提供程序进行集成。
如:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
var fileServerOptions = new FileServerOptions();
var fileProvider = new CompositeFileProvider(
env.WebRootFileProvider,
new ManifestEmbeddedFileProvider(Assembly.GetExecutingAssembly(), "/files")
);
fileServerOptions.StaticFileOptions.FileProvider = fileProvider;
fileServerOptions.StaticFileOptions.RequestPath = "/composite";
app.UseFileServer(fileServerOptions);
}
现在,你可以通过 http://localhost:5000/composite/file.json 来访问文件了。
ContentTypeProvider
Http请求头中的Content-Type
大家一定很熟悉,ContentTypeProvider
就是用来提供文件扩展名和MIME类型映射关系的。
若我们没有显示指定ContentTypeProvider
,则框架默认使用FileExtensionContentTypeProvider
,其实现了接口IContentTypeProvider
:
public interface IContentTypeProvider
{
// 尝试根据文件路径,获取对应的 MIME 类型
bool TryGetContentType(string subpath, out string contentType);
}
public class FileExtensionContentTypeProvider : IContentTypeProvider
{
public FileExtensionContentTypeProvider()
: this(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
// ...此处省略一万字
}
{
}
public FileExtensionContentTypeProvider(IDictionary<string, string> mapping)
{
Mappings = mapping;
}
public IDictionary<string, string> Mappings { get; private set; }
public bool TryGetContentType(string subpath, out string contentType)
{
string extension = GetExtension(subpath);
if (extension == null)
{
contentType = null;
return false;
}
return Mappings.TryGetValue(extension, out contentType);
}
private static string GetExtension(string path)
{
// 没有使用 Path.GetExtension() 的原因是:当路径中存在无效字符时,其会抛出异常,而这里不应抛出异常。
if (string.IsNullOrWhiteSpace(path))
{
return null;
}
int index = path.LastIndexOf('.');
if (index < 0)
{
return null;
}
return path.Substring(index);
}
}
在FileExtensionContentTypeProvider
的无参构造函数中,默认添加了380种已知的文件扩展名和MIME类型的映射,存放在Mappings
属性中。你也可以添加自定义的映射,或移除不想要的映射。
核心中间件
StaticFileMiddleware
通过UseStaticFiles
扩展方法,可以方便的注册StaticFileMiddleware
中间件:
public static class StaticFileExtensions
{
public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app)
{
return app.UseMiddleware<StaticFileMiddleware>();
}
public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, string requestPath)
{
return app.UseStaticFiles(new StaticFileOptions
{
RequestPath = new PathString(requestPath)
});
}
public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, StaticFileOptions options)
{
return app.UseMiddleware<StaticFileMiddleware>(Options.Create(options));
}
}
紧接着查看StaticFileMiddleware
的Invoke
方法:
public class StaticFileMiddleware
{
private readonly StaticFileOptions _options;
private readonly PathString _matchUrl;
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly IFileProvider _fileProvider;
private readonly IContentTypeProvider _contentTypeProvider;
public StaticFileMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<StaticFileOptions> options, ILoggerFactory loggerFactory)
{
_next = next;
_options = options.Value;
// 若未指定 ContentTypeProvider,则默认使用 FileExtensionContentTypeProvider
_contentTypeProvider = _options.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
// 若未指定 FileProvider,则默认使用 hostingEnv.WebRootFileProvider
_fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
_matchUrl = _options.RequestPath;
_logger = loggerFactory.CreateLogger<StaticFileMiddleware>();
}
public Task Invoke(HttpContext context)
{
// 若已匹配到 Endpoint,则跳过
if (!ValidateNoEndpoint(context))
{
_logger.EndpointMatched();
}
// 若HTTP请求方法不是 Get,也不是 Head,则跳过
else if (!ValidateMethod(context))
{
_logger.RequestMethodNotSupported(context.Request.Method);
}
// 如果请求路径不匹配,则跳过
else if (!ValidatePath(context, _matchUrl, out var subPath))
{
_logger.PathMismatch(subPath);
}
// 如果 ContentType 不受支持,则跳过
else if (!LookupContentType(_contentTypeProvider, _options, subPath, out var contentType))
{
_logger.FileTypeNotSupported(subPath);
}
else
{
// 尝试提供静态文件
return TryServeStaticFile(context, contentType, subPath);
}
return _next(context);
}
private static bool ValidateNoEndpoint(HttpContext context) => context.GetEndpoint() == null;
private static bool ValidateMethod(HttpContext context) => Helpers.IsGetOrHeadMethod(context.Request.Method);
internal static bool ValidatePath(HttpContext context, PathString matchUrl, out PathString subPath) => Helpers.TryMatchPath(context, matchUrl, forDirectory: false, out subPath);
internal static bool LookupContentType(IContentTypeProvider contentTypeProvider, StaticFileOptions options, PathString subPath, out string contentType)
{
// 查看 Provider 中是否支持该 ContentType
if (contentTypeProvider.TryGetContentType(subPath.Value, out contentType))
{
return true;
}
// 如果提供未知文件类型,则将其设置为默认 ContentType
if (options.ServeUnknownFileTypes)
{
contentType = options.DefaultContentType;
return true;
}
return false;
}
private Task TryServeStaticFile(HttpContext context, string contentType, PathString subPath)
{
var fileContext = new StaticFileContext(context, _options, _logger, _fileProvider, contentType, subPath);
// 如果文件不存在,则跳过
if (!fileContext.LookupFileInfo())
{
_logger.FileNotFound(fileContext.SubPath);
}
else
{
// 若文件存在,则提供该静态文件
return fileContext.ServeStaticFile(context, _next);
}
return _next(context);
}
}
DirectoryBrowserMiddleware
通过UseDirectoryBrowser
扩展方法,可以方便的注册DirectoryBrowserMiddleware
中间件:
public static class DirectoryBrowserExtensions
{
public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app)
{
return app.UseMiddleware<DirectoryBrowserMiddleware>();
}
public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, string requestPath)
{
return app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
RequestPath = new PathString(requestPath)
});
}
public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, DirectoryBrowserOptions options)
{
return app.UseMiddleware<DirectoryBrowserMiddleware>(Options.Create(options));
}
}
紧接着查看DirectoryBrowserMiddleware
的Invoke
方法:
public class DirectoryBrowserMiddleware
{
private readonly DirectoryBrowserOptions _options;
private readonly PathString _matchUrl;
private readonly RequestDelegate _next;
private readonly IDirectoryFormatter _formatter;
private readonly IFileProvider _fileProvider;
public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<DirectoryBrowserOptions> options)
: this(next, hostingEnv, HtmlEncoder.Default, options)
{
}
public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, HtmlEncoder encoder, IOptions<DirectoryBrowserOptions> options)
{
_next = next;
_options = options.Value;
// 若未指定 FileProvider,则默认使用 hostingEnv.WebRootFileProvider
_fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
_formatter = _options.Formatter ?? new HtmlDirectoryFormatter(encoder);
_matchUrl = _options.RequestPath;
}
public Task Invoke(HttpContext context)
{
// 若已匹配到 Endpoint,则跳过
// 若HTTP请求方法不是 Get,也不是 Head,则跳过
// 如果请求路径不匹配,则跳过
// 若文件目录不存在,则跳过
if (context.GetEndpoint() == null
&& Helpers.IsGetOrHeadMethod(context.Request.Method)
&& Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath)
&& TryGetDirectoryInfo(subpath, out var contents))
{
if (_options.RedirectToAppendTrailingSlash && !Helpers.PathEndsInSlash(context.Request.Path))
{
Helpers.RedirectToPathWithSlash(context);
return Task.CompletedTask;
}
// 生成文件浏览视图
return _formatter.GenerateContentAsync(context, contents);
}
return _next(context);
}
private bool TryGetDirectoryInfo(PathString subpath, out IDirectoryContents contents)
{
contents = _fileProvider.GetDirectoryContents(subpath.Value);
return contents.Exists;
}
}
DefaultFilesMiddleware
通过UseDefaultFiles
扩展方法,可以方便的注册DefaultFilesMiddleware
中间件:
public static class DefaultFilesExtensions
{
public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app)
{
return app.UseMiddleware<DefaultFilesMiddleware>();
}
public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, string requestPath)
{
return app.UseDefaultFiles(new DefaultFilesOptions
{
RequestPath = new PathString(requestPath)
});
}
public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, DefaultFilesOptions options)
{
return app.UseMiddleware<DefaultFilesMiddleware>(Options.Create(options));
}
}
紧接着查看DefaultFilesMiddleware
的Invoke
方法:
public class DefaultFilesMiddleware
{
private readonly DefaultFilesOptions _options;
private readonly PathString _matchUrl;
private readonly RequestDelegate _next;
private readonly IFileProvider _fileProvider;
public DefaultFilesMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<DefaultFilesOptions> options)
{
_next = next;
_options = options.Value;
// 若未指定 FileProvider,则默认使用 hostingEnv.WebRootFileProvider
_fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
_matchUrl = _options.RequestPath;
}
public Task Invoke(HttpContext context)
{
// 若已匹配到 Endpoint,则跳过
// 若HTTP请求方法不是 Get,也不是 Head,则跳过
// 如果请求路径不匹配,则跳过
if (context.GetEndpoint() == null
&& Helpers.IsGetOrHeadMethod(context.Request.Method)
&& Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath))
{
var dirContents = _fileProvider.GetDirectoryContents(subpath.Value);
if (dirContents.Exists)
{
for (int matchIndex = 0; matchIndex < _options.DefaultFileNames.Count; matchIndex++)
{
string defaultFile = _options.DefaultFileNames[matchIndex];
var file = _fileProvider.GetFileInfo(subpath.Value + defaultFile);
// 找到了默认页
if (file.Exists)
{
if (_options.RedirectToAppendTrailingSlash && !Helpers.PathEndsInSlash(context.Request.Path))
{
Helpers.RedirectToPathWithSlash(context);
return Task.CompletedTask;
}
// 重写为默认页的Url,后续通过 StaticFileMiddleware 提供该页面
context.Request.Path = new PathString(Helpers.GetPathValueWithSlash(context.Request.Path) + defaultFile);
break;
}
}
}
}
return _next(context);
}
}
FileServer
FileServer并不是某个具体的中间件,它的实现还是依赖了StaticFileMiddleware
、DirectoryBrowserMiddleware
和DefaultFilesMiddleware
这3个中间件。不过,我们可以看一下UseFileServer
里的逻辑:
public static class FileServerExtensions
{
public static IApplicationBuilder UseFileServer(this IApplicationBuilder app)
{
return app.UseFileServer(new FileServerOptions());
}
public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, bool enableDirectoryBrowsing)
{
return app.UseFileServer(new FileServerOptions
{
EnableDirectoryBrowsing = enableDirectoryBrowsing
});
}
public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, string requestPath)
{
return app.UseFileServer(new FileServerOptions
{
RequestPath = new PathString(requestPath)
});
}
public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, FileServerOptions options)
{
// 启用默认页
if (options.EnableDefaultFiles)
{
app.UseDefaultFiles(options.DefaultFilesOptions);
}
// 启用目录浏览
if (options.EnableDirectoryBrowsing)
{
app.UseDirectoryBrowser(options.DirectoryBrowserOptions);
}
return app.UseStaticFiles(options.StaticFileOptions);
}
}
FileProvider in IWebHostingEnvironment
在接口IHostingEnvironment
中,包含ContentRootFileProvider
和WebRootFileProvider
两个文件提供程序。下面我们就看一下他们是如何被初始化的。
internal class GenericWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider
{
private WebHostBuilderContext GetWebHostBuilderContext(HostBuilderContext context)
{
if (!context.Properties.TryGetValue(typeof(WebHostBuilderContext), out var contextVal))
{
var options = new WebHostOptions(context.Configuration, Assembly.GetEntryAssembly()?.GetName().Name);
var webHostBuilderContext = new WebHostBuilderContext
{
Configuration = context.Configuration,
HostingEnvironment = new HostingEnvironment(),
};
// 重点在这里,看这个 Initialize 方法
webHostBuilderContext.HostingEnvironment.Initialize(context.HostingEnvironment.ContentRootPath, options);
context.Properties[typeof(WebHostBuilderContext)] = webHostBuilderContext;
context.Properties[typeof(WebHostOptions)] = options;
return webHostBuilderContext;
}
var webHostContext = (WebHostBuilderContext)contextVal;
webHostContext.Configuration = context.Configuration;
return webHostContext;
}
}
internal static class HostingEnvironmentExtensions
{
internal static void Initialize(this IWebHostEnvironment hostingEnvironment, string contentRootPath, WebHostOptions options)
{
hostingEnvironment.ApplicationName = options.ApplicationName;
hostingEnvironment.ContentRootPath = contentRootPath;
// 初始化 ContentRootFileProvider
hostingEnvironment.ContentRootFileProvider = new PhysicalFileProvider(hostingEnvironment.ContentRootPath);
var webRoot = options.WebRoot;
if (webRoot == null)
{
// 如果 /wwwroot 目录存在,则设置为Web根目录
var wwwroot = Path.Combine(hostingEnvironment.ContentRootPath, "wwwroot");
if (Directory.Exists(wwwroot))
{
hostingEnvironment.WebRootPath = wwwroot;
}
}
else
{
hostingEnvironment.WebRootPath = Path.Combine(hostingEnvironment.ContentRootPath, webRoot);
}
if (!string.IsNullOrEmpty(hostingEnvironment.WebRootPath))
{
hostingEnvironment.WebRootPath = Path.GetFullPath(hostingEnvironment.WebRootPath);
if (!Directory.Exists(hostingEnvironment.WebRootPath))
{
Directory.CreateDirectory(hostingEnvironment.WebRootPath);
}
// 初始化 WebRootFileProvider
hostingEnvironment.WebRootFileProvider = new PhysicalFileProvider(hostingEnvironment.WebRootPath);
}
else
{
hostingEnvironment.WebRootFileProvider = new NullFileProvider();
}
hostingEnvironment.EnvironmentName =
options.Environment ??
hostingEnvironment.EnvironmentName;
}
}
注意
- 使用
UseDirectoryBrowser
和UseStaticFiles
提供文件浏览和访问时,URL 受大小写和基础文件系统字符的限制。例如,Windows 不区分大小写,但 macOS 和 Linux 区分大小写。 - 如果使用 IIS 托管应用,那么 IIS 自带的静态文件处理器是不工作的,均是使用 ASP.NET Core Module 进行处理的,包括静态文件处理。
小结
- 使用
UseFileServer
扩展方法提供文件浏览和访问,其集成了UseStaticFiles
、UseDirectoryBrowser
和UseDefaultFiles
三个中间件的功能。UseStaticFiles
:注册StaticFilesMiddleware
,提供文件访问UseDirectoryBrowser
:注册DirectoryBrowserMiddleware
,提供文件目录浏览UseDefaultFiles
:注册DefaultFilesMiddleware
,当Url未指定访问的文件名时,提供默认页。
- 文件提供程序均实现了接口
IFileProvider
,常用的文件提供程序有以下三种:PhysicalFileProvider
:提供物理文件系统的访问ManifestEmbeddedFileProvider
:提供嵌入在程序集中的文件的访问CompositeFileProvider
:用于将多种文件提供程序进行集成。
- 可通过
IWebHostingEnvironment
获取ContentRootFileProvider
(默认目录为项目根目录)和WebRootFileProvider
(默认目录为Web根目录)。