注:本文隸屬於《理解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根目錄)。