注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录

提供静态文件

静态文件默认存放在 Web根目录(Web Root) 中,路径为 项目根目录(Content Root) 下的wwwroot文件夹,也就是{Content Root}/wwwroot

如果你调用了Host.CreateDefaultBuilder方法,那么在该方法中,会通过UseContentRoot方法,将程序当前工作目录(Directory.GetCurrentDirectory())设置为项目根目录。具体可以查看主机一节。

  1. public static IHostBuilder CreateHostBuilder(string[] args) =>
  2. Host.CreateDefaultBuilder(args)
  3. .ConfigureWebHostDefaults(webBuilder =>
  4. {
  5. webBuilder.UseStartup<Startup>();
  6. });

当然,你也可以通过UseWebRoot扩展方法将默认的路径{Content Root}/wwwroot修改为自定义目录(不过,你改它干啥捏?)

  1. public static IHostBuilder CreateHostBuilder(string[] args) =>
  2. Host.CreateDefaultBuilder(args)
  3. .ConfigureWebHostDefaults(webBuilder =>
  4. {
  5. // 配置静态资源的根目录为 mywwwroot, 默认为 wwwroot
  6. webBuilder.UseWebRoot("mywwwroot");
  7. webBuilder.UseStartup<Startup>();
  8. });

为了方便,后面均使用 wwwroot 来表示Web根目录

首先,我们先在 wwwroot 文件夹下创建一个名为 config.json 的文件,内容随便填写

注意,确保 wwwroot 下的文件的属性为“如果较新则复制”或“始终复制”。

接着,我们通过UseStaticFiles扩展方法,来注册静态文件中间件StaticFileMiddleware

  1. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  2. {
  3. app.UseStaticFiles();
  4. }

现在,尝试一下通过 http://localhost:5000/config.json 来获取 wwwroot/config.json 的文件内容吧

如果你的项目中启用SwaggerUI,那么你会发现,即使你没有手动通过调用UseStaticFiles()添加中间件,你也可以访问 wwwroot 文件下的文件,这是因为 SwaggerUIMiddleware 中使用了 StaticFileMiddleware

提供Web根目录之外的文件

上面我们已经能够提供 wwwroot 文件夹内的静态文件了,那如果我们的文件不在 wwwroot 文件夹内,那如何提供呢?

很简单,我们可以针对StaticFileMiddleware中间件进行一些额外的配置,了解一下配置项:

  1. public abstract class SharedOptionsBase
  2. {
  3. // 用于自定义静态文件的相对请求路径
  4. public PathString RequestPath { get; set; }
  5. // 文件提供程序
  6. public IFileProvider FileProvider { get; set; }
  7. // 是否补全路径末尾斜杠“/”,并重定向
  8. public bool RedirectToAppendTrailingSlash { get; set; }
  9. }
  10. public class StaticFileOptions : SharedOptionsBase
  11. {
  12. // ContentType提供程序
  13. public IContentTypeProvider ContentTypeProvider { get; set; }
  14. // 如果 ContentTypeProvider 无法识别文件类型,是否仍作为默认文件类型提供
  15. public bool ServeUnknownFileTypes { get; set; }
  16. // 当 ServeUnknownFileTypes = true 时,若出现无法识别的文件类型,则将该属性的值作为此文件的类型
  17. // 当 ServeUnknownFileTypes = true 时,必须赋值该属性,才会生效
  18. public string DefaultContentType { get; set; }
  19. // 当注册了HTTP响应压缩中间件时,是否对文件进行压缩
  20. public HttpsCompressionMode HttpsCompression { get; set; } = HttpsCompressionMode.Compress;
  21. // 在HTTP响应的 Status Code 和 Headers 设置完毕之后,Body 写入之前进行调用
  22. // 用于添加或更改 Headers
  23. public Action<StaticFileResponseContext> OnPrepareResponse { get; set; }
  24. }

假设我们现在有这样一个文件目录结构:

  • wwwroot

    • config.json
  • files
    • file.json

然后,除了用于提供 wwwroot 静态文件的中间件外,我们还要注册一个用于提供 files 静态文件的中间件:

  1. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  2. {
  3. // 提供 wwwroot 静态文件
  4. app.UseStaticFiles();
  5. // 提供 files 静态文件
  6. app.UseStaticFiles(new StaticFileOptions
  7. {
  8. FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files")),
  9. // 指定文件的访问路径,允许与 FileProvider 中的文件夹不同名
  10. // 如果不指定,则可通过 http://localhost:5000/file.json 获取,
  11. // 如果指定,则需要通过 http://localhost:5000/files/file.json 获取
  12. RequestPath = "/files",
  13. OnPrepareResponse = ctx =>
  14. {
  15. // 配置前端缓存 600s(为了后续示例的良好运行,建议先不要配置该Header)
  16. ctx.Context.Response.Headers.Add(HeaderNames.CacheControl, "public,max-age=600");
  17. }
  18. });
  19. }

建议将公开访问的文件放置到 wwwroot 目录下,而将需要授权访问的文件放置到其他目录下(在调用UseAuthorization之后调用UseStaticFiles并指定文件目录)

提供目录浏览

上面,我们可以通过Url访问某一个文件的内容,而通过UseDirectoryBrowser,注册DirectoryBrowserMiddleware中间件,可以让我们在浏览器中以目录的形式来访问文件列表。

另外,DirectoryBrowserMiddleware中间件的可配置项除了SharedOptionsBase中的之外,还有一个Formatter,用于自定义目录视图。

  1. public class DirectoryBrowserOptions : SharedOptionsBase
  2. {
  3. public IDirectoryFormatter Formatter { get; set; }
  4. }

示例如下:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddDirectoryBrowser();
  4. }
  5. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  6. {
  7. // 通过 http://localhost:5000,即可访问 wwwroot 目录
  8. app.UseDirectoryBrowser();
  9. // 通过 http://localhost:5000/files,即可访问 files 目录
  10. app.UseDirectoryBrowser(new DirectoryBrowserOptions
  11. {
  12. // 如果指定了没有在 UseStaticFiles 中提供的文件目录,虽然可以浏览文件列表,但是无法访问文件内容
  13. FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files")),
  14. // 这里一定要和 StaticFileOptions 中的 RequestPath 一致,否则会无法访问文件
  15. RequestPath = "/files"
  16. });
  17. }

提供默认页

通过UseDefaultFiles,注册DefaultFilesMiddleware中间件,允许在访问静态文件、但未提供文件名的情况下(即传入的是一个目录的路径),提供默认页的展示。

注意:UseDefaultFiles必须在UseStaticFiles之前进行调用。因为DefaultFilesMiddleware仅仅负责重写Url,实际上默认页文件,仍然是通过StaticFilesMiddleware来提供的。

默认情况下,该中间件会按照顺序搜索文件目录下的HTML页面文件:

  • default.htm
  • default.html
  • index.htm
  • index.html

另外,DefaultFilesMiddleware中间件的可配置项除了SharedOptionsBase中的之外,还有一个DefaultFileNames,是个列表,用于自定义默认页的文件名,里面的默认值就是上面提到的4个文件名。

  1. public class DefaultFilesOptions : SharedOptionsBase
  2. {
  3. public IList<string> DefaultFileNames { get; set; }
  4. }

示例如下:

  1. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  2. {
  3. // 会去 wwwroot 寻找 default.htm 、default.html 、index.htm 或 index.html 文件作为默认页
  4. app.UseDefaultFiles();
  5. // 设置 files 目录的默认页
  6. var defaultFilesOptions = new DefaultFilesOptions();
  7. defaultFilesOptions.DefaultFileNames.Clear();
  8. // 指定默认页名称
  9. defaultFilesOptions.DefaultFileNames.Add("index1.html");
  10. // 指定请求路径
  11. defaultFilesOptions.RequestPath = "/files";
  12. // 指定默认页所在的目录
  13. defaultFilesOptions.FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files"));
  14. app.UseDefaultFiles(defaultFilesOptions);
  15. }

UseFileServer

UseFileServer集成了UseStaticFilesUseDefaultFilesUseDirectoryBrowser的功能,用起来方便一些,也是我们项目中使用的首选扩展方法。

先看一下FileServerOptions

  1. public class FileServerOptions : SharedOptionsBase
  2. {
  3. public FileServerOptions()
  4. : base(new SharedOptions())
  5. {
  6. StaticFileOptions = new StaticFileOptions(SharedOptions);
  7. DirectoryBrowserOptions = new DirectoryBrowserOptions(SharedOptions);
  8. DefaultFilesOptions = new DefaultFilesOptions(SharedOptions);
  9. EnableDefaultFiles = true;
  10. }
  11. public StaticFileOptions StaticFileOptions { get; private set; }
  12. public DirectoryBrowserOptions DirectoryBrowserOptions { get; private set; }
  13. public DefaultFilesOptions DefaultFilesOptions { get; private set; }
  14. // 默认禁用目录浏览
  15. public bool EnableDirectoryBrowsing { get; set; }
  16. // 默认启用默认页(在构造函数中初始化的)
  17. public bool EnableDefaultFiles { get; set; }
  18. }

可以看到,FileServerOptions包含了StaticFileOptionsDirectoryBrowserOptionsDefaultFilesOptions三个选项,可以针对StaticFileMiddlewareDirectoryBrowserMiddlewareDefaultFilesMiddleware进行自定义配置。另外,其默认启用了静态文件和默认页,禁用了目录浏览。

下面举个例子熟悉一下:

假设文件目录:

  • files

    • images

      • 1.jpg
    • file.json
    • myindex.html
  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. // 如果将 EnableDirectoryBrowsing 设为 true,记得注册服务
  4. services.AddDirectoryBrowser();
  5. }
  6. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  7. {
  8. // 启用 StaticFileMiddleware
  9. // 启用 DefaultFilesMiddleware
  10. // 禁用 DirectoryBrowserMiddleware
  11. // 默认指向 wwwroot
  12. app.UseFileServer();
  13. // 针对 files 文件夹配置
  14. var fileServerOptions = new FileServerOptions
  15. {
  16. FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files")),
  17. RequestPath = "/files",
  18. EnableDirectoryBrowsing = true
  19. };
  20. fileServerOptions.StaticFileOptions.OnPrepareResponse = ctx =>
  21. {
  22. // 配置缓存600s
  23. ctx.Context.Response.Headers.Add(HeaderNames.CacheControl, "public,max-age=600");
  24. };
  25. fileServerOptions.DefaultFilesOptions.DefaultFileNames.Clear();
  26. fileServerOptions.DefaultFilesOptions.DefaultFileNames.Add("myindex.html");
  27. app.UseFileServer(fileServerOptions);
  28. }

当访问 http://localhost:5000/files 时,由于在DefaultFilesOptions.DefaultFileNames中添加了文件名myindex.html,所以可以找到默认页,此时会显示默认页的内容。

假如我们没有在DefaultFilesOptions.DefaultFileNames中添加文件名myindex.html,那么便找不到默认页,但由于启用了DirectoryBrowsing,所以此时会展示文件列表。

核心配置项

FileProvider

上面我们已经见过PhysicalFileProvider了,它仅仅是众多文件提供程序中的一种。所有的文件提供程序均实现了IFileProvider接口:

  1. public interface IFileProvider
  2. {
  3. // 获取给定路径的目录信息,可枚举该目录中的所有文件
  4. IDirectoryContents GetDirectoryContents(string subpath);
  5. // 获取给定路径的文件信息
  6. IFileInfo GetFileInfo(string subpath);
  7. // 创建指定 filter 的 ChangeToken
  8. IChangeToken Watch(string filter);
  9. }
  10. public interface IDirectoryContents : IEnumerable<IFileInfo>, IEnumerable
  11. {
  12. bool Exists { get; }
  13. }
  14. public interface IFileInfo
  15. {
  16. bool Exists { get; }
  17. bool IsDirectory { get; }
  18. DateTimeOffset LastModified { get; }
  19. // 字节(bytes)长度
  20. // 如果是目录或文件不存在,则是 -1
  21. long Length { get; }
  22. // 目录或文件名,纯文件名,不包括路径
  23. string Name { get; }
  24. // 文件路径,包含文件名
  25. // 如果文件无法直接访问,则返回 null
  26. string PhysicalPath { get; }
  27. // 创建该文件只读流
  28. Stream CreateReadStream();
  29. }

常用的文件提供程序有以下三种:

  • PhysicalFileProvider
  • ManifestEmbeddedFileProvider
  • CompositeFileProvider

glob模式

在介绍这三种文件提供程序之前,先说一下glob模式,即通配符模式。两个通配符分别是***

  • *:匹配当前目录层级(不包含子目录)下的任何内容、任何文件名或任何文件扩展名,可以通过/\.进行分隔。
  • **:匹配目录多层级(包含子目录)的任何内容,用于递归匹配多层级目录的多个文件。

PhysicalFileProvider

PhysicalFileProvider用于提供物理文件系统的访问。该提供程序需要将文件路径范围限定在一个目录及其子目录中,不能访问目录外部的内容。

当实例化该文件提供程序时,需要提供一个绝对的目录路径,作为文件目录的root。

PhysicalFileProvider目录或文件路径不支持glob(通配符)模式。

ManifestEmbeddedFileProvider

ManifestEmbeddedFileProvider用于提供嵌入在程序集中的文件的访问。

可能你对这个嵌入文件比较陌生,没关系,请按照下面的步骤来:

  • 安装Nuget包:Install-Package Microsoft.Extensions.FileProviders.Embedded
  • 编辑.csproj文件:
    • 添加<GenerateEmbeddedFilesManifest>,并设置为true
    • 使用<EmbeddedResource>添加要嵌入的文件

以下是 .csproj 文件的示例:

  1. <Project Sdk="Microsoft.NET.Sdk.Web">
  2. <PropertyGroup>
  3. <TargetFramework>net5.0</TargetFramework>
  4. <GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
  5. </PropertyGroup>
  6. <ItemGroup>
  7. <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="5.0.11" />
  8. </ItemGroup>
  9. <ItemGroup>
  10. <EmbeddedResource Include="files\**" />
  11. </ItemGroup>
  12. </Project>

现在我们通过ManifestEmbeddedFileProvider来提供嵌入到程序集的 files 目录下文件的访问:

  1. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  2. {
  3. var fileServerOptions = new FileServerOptions();
  4. fileServerOptions.StaticFileOptions.FileProvider = new ManifestEmbeddedFileProvider(Assembly.GetExecutingAssembly(), "/files");
  5. fileServerOptions.StaticFileOptions.RequestPath = "/files";
  6. app.UseFileServer(fileServerOptions);
  7. }

现在,你可以通过 http://localhost:5000/files/file.json 来访问文件了。

CompositeFileProvider

CompositeFileProvider用于将多种文件提供程序进行集成。

如:

  1. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  2. {
  3. var fileServerOptions = new FileServerOptions();
  4. var fileProvider = new CompositeFileProvider(
  5. env.WebRootFileProvider,
  6. new ManifestEmbeddedFileProvider(Assembly.GetExecutingAssembly(), "/files")
  7. );
  8. fileServerOptions.StaticFileOptions.FileProvider = fileProvider;
  9. fileServerOptions.StaticFileOptions.RequestPath = "/composite";
  10. app.UseFileServer(fileServerOptions);
  11. }

现在,你可以通过 http://localhost:5000/composite/file.json 来访问文件了。

ContentTypeProvider

Http请求头中的Content-Type大家一定很熟悉,ContentTypeProvider就是用来提供文件扩展名和MIME类型映射关系的。

若我们没有显示指定ContentTypeProvider,则框架默认使用FileExtensionContentTypeProvider,其实现了接口IContentTypeProvider

  1. public interface IContentTypeProvider
  2. {
  3. // 尝试根据文件路径,获取对应的 MIME 类型
  4. bool TryGetContentType(string subpath, out string contentType);
  5. }
  6. public class FileExtensionContentTypeProvider : IContentTypeProvider
  7. {
  8. public FileExtensionContentTypeProvider()
  9. : this(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
  10. {
  11. // ...此处省略一万字
  12. }
  13. {
  14. }
  15. public FileExtensionContentTypeProvider(IDictionary<string, string> mapping)
  16. {
  17. Mappings = mapping;
  18. }
  19. public IDictionary<string, string> Mappings { get; private set; }
  20. public bool TryGetContentType(string subpath, out string contentType)
  21. {
  22. string extension = GetExtension(subpath);
  23. if (extension == null)
  24. {
  25. contentType = null;
  26. return false;
  27. }
  28. return Mappings.TryGetValue(extension, out contentType);
  29. }
  30. private static string GetExtension(string path)
  31. {
  32. // 没有使用 Path.GetExtension() 的原因是:当路径中存在无效字符时,其会抛出异常,而这里不应抛出异常。
  33. if (string.IsNullOrWhiteSpace(path))
  34. {
  35. return null;
  36. }
  37. int index = path.LastIndexOf('.');
  38. if (index < 0)
  39. {
  40. return null;
  41. }
  42. return path.Substring(index);
  43. }
  44. }

FileExtensionContentTypeProvider的无参构造函数中,默认添加了380种已知的文件扩展名和MIME类型的映射,存放在Mappings属性中。你也可以添加自定义的映射,或移除不想要的映射。

核心中间件

StaticFileMiddleware

通过UseStaticFiles扩展方法,可以方便的注册StaticFileMiddleware中间件:

  1. public static class StaticFileExtensions
  2. {
  3. public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app)
  4. {
  5. return app.UseMiddleware<StaticFileMiddleware>();
  6. }
  7. public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, string requestPath)
  8. {
  9. return app.UseStaticFiles(new StaticFileOptions
  10. {
  11. RequestPath = new PathString(requestPath)
  12. });
  13. }
  14. public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, StaticFileOptions options)
  15. {
  16. return app.UseMiddleware<StaticFileMiddleware>(Options.Create(options));
  17. }
  18. }

紧接着查看StaticFileMiddlewareInvoke方法:

  1. public class StaticFileMiddleware
  2. {
  3. private readonly StaticFileOptions _options;
  4. private readonly PathString _matchUrl;
  5. private readonly RequestDelegate _next;
  6. private readonly ILogger _logger;
  7. private readonly IFileProvider _fileProvider;
  8. private readonly IContentTypeProvider _contentTypeProvider;
  9. public StaticFileMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<StaticFileOptions> options, ILoggerFactory loggerFactory)
  10. {
  11. _next = next;
  12. _options = options.Value;
  13. // 若未指定 ContentTypeProvider,则默认使用 FileExtensionContentTypeProvider
  14. _contentTypeProvider = _options.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
  15. // 若未指定 FileProvider,则默认使用 hostingEnv.WebRootFileProvider
  16. _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
  17. _matchUrl = _options.RequestPath;
  18. _logger = loggerFactory.CreateLogger<StaticFileMiddleware>();
  19. }
  20. public Task Invoke(HttpContext context)
  21. {
  22. // 若已匹配到 Endpoint,则跳过
  23. if (!ValidateNoEndpoint(context))
  24. {
  25. _logger.EndpointMatched();
  26. }
  27. // 若HTTP请求方法不是 Get,也不是 Head,则跳过
  28. else if (!ValidateMethod(context))
  29. {
  30. _logger.RequestMethodNotSupported(context.Request.Method);
  31. }
  32. // 如果请求路径不匹配,则跳过
  33. else if (!ValidatePath(context, _matchUrl, out var subPath))
  34. {
  35. _logger.PathMismatch(subPath);
  36. }
  37. // 如果 ContentType 不受支持,则跳过
  38. else if (!LookupContentType(_contentTypeProvider, _options, subPath, out var contentType))
  39. {
  40. _logger.FileTypeNotSupported(subPath);
  41. }
  42. else
  43. {
  44. // 尝试提供静态文件
  45. return TryServeStaticFile(context, contentType, subPath);
  46. }
  47. return _next(context);
  48. }
  49. private static bool ValidateNoEndpoint(HttpContext context) => context.GetEndpoint() == null;
  50. private static bool ValidateMethod(HttpContext context) => Helpers.IsGetOrHeadMethod(context.Request.Method);
  51. internal static bool ValidatePath(HttpContext context, PathString matchUrl, out PathString subPath) => Helpers.TryMatchPath(context, matchUrl, forDirectory: false, out subPath);
  52. internal static bool LookupContentType(IContentTypeProvider contentTypeProvider, StaticFileOptions options, PathString subPath, out string contentType)
  53. {
  54. // 查看 Provider 中是否支持该 ContentType
  55. if (contentTypeProvider.TryGetContentType(subPath.Value, out contentType))
  56. {
  57. return true;
  58. }
  59. // 如果提供未知文件类型,则将其设置为默认 ContentType
  60. if (options.ServeUnknownFileTypes)
  61. {
  62. contentType = options.DefaultContentType;
  63. return true;
  64. }
  65. return false;
  66. }
  67. private Task TryServeStaticFile(HttpContext context, string contentType, PathString subPath)
  68. {
  69. var fileContext = new StaticFileContext(context, _options, _logger, _fileProvider, contentType, subPath);
  70. // 如果文件不存在,则跳过
  71. if (!fileContext.LookupFileInfo())
  72. {
  73. _logger.FileNotFound(fileContext.SubPath);
  74. }
  75. else
  76. {
  77. // 若文件存在,则提供该静态文件
  78. return fileContext.ServeStaticFile(context, _next);
  79. }
  80. return _next(context);
  81. }
  82. }

DirectoryBrowserMiddleware

通过UseDirectoryBrowser扩展方法,可以方便的注册DirectoryBrowserMiddleware中间件:

  1. public static class DirectoryBrowserExtensions
  2. {
  3. public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app)
  4. {
  5. return app.UseMiddleware<DirectoryBrowserMiddleware>();
  6. }
  7. public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, string requestPath)
  8. {
  9. return app.UseDirectoryBrowser(new DirectoryBrowserOptions
  10. {
  11. RequestPath = new PathString(requestPath)
  12. });
  13. }
  14. public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, DirectoryBrowserOptions options)
  15. {
  16. return app.UseMiddleware<DirectoryBrowserMiddleware>(Options.Create(options));
  17. }
  18. }

紧接着查看DirectoryBrowserMiddlewareInvoke方法:

  1. public class DirectoryBrowserMiddleware
  2. {
  3. private readonly DirectoryBrowserOptions _options;
  4. private readonly PathString _matchUrl;
  5. private readonly RequestDelegate _next;
  6. private readonly IDirectoryFormatter _formatter;
  7. private readonly IFileProvider _fileProvider;
  8. public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<DirectoryBrowserOptions> options)
  9. : this(next, hostingEnv, HtmlEncoder.Default, options)
  10. {
  11. }
  12. public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, HtmlEncoder encoder, IOptions<DirectoryBrowserOptions> options)
  13. {
  14. _next = next;
  15. _options = options.Value;
  16. // 若未指定 FileProvider,则默认使用 hostingEnv.WebRootFileProvider
  17. _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
  18. _formatter = _options.Formatter ?? new HtmlDirectoryFormatter(encoder);
  19. _matchUrl = _options.RequestPath;
  20. }
  21. public Task Invoke(HttpContext context)
  22. {
  23. // 若已匹配到 Endpoint,则跳过
  24. // 若HTTP请求方法不是 Get,也不是 Head,则跳过
  25. // 如果请求路径不匹配,则跳过
  26. // 若文件目录不存在,则跳过
  27. if (context.GetEndpoint() == null
  28. && Helpers.IsGetOrHeadMethod(context.Request.Method)
  29. && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath)
  30. && TryGetDirectoryInfo(subpath, out var contents))
  31. {
  32. if (_options.RedirectToAppendTrailingSlash && !Helpers.PathEndsInSlash(context.Request.Path))
  33. {
  34. Helpers.RedirectToPathWithSlash(context);
  35. return Task.CompletedTask;
  36. }
  37. // 生成文件浏览视图
  38. return _formatter.GenerateContentAsync(context, contents);
  39. }
  40. return _next(context);
  41. }
  42. private bool TryGetDirectoryInfo(PathString subpath, out IDirectoryContents contents)
  43. {
  44. contents = _fileProvider.GetDirectoryContents(subpath.Value);
  45. return contents.Exists;
  46. }
  47. }

DefaultFilesMiddleware

通过UseDefaultFiles扩展方法,可以方便的注册DefaultFilesMiddleware中间件:

  1. public static class DefaultFilesExtensions
  2. {
  3. public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app)
  4. {
  5. return app.UseMiddleware<DefaultFilesMiddleware>();
  6. }
  7. public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, string requestPath)
  8. {
  9. return app.UseDefaultFiles(new DefaultFilesOptions
  10. {
  11. RequestPath = new PathString(requestPath)
  12. });
  13. }
  14. public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, DefaultFilesOptions options)
  15. {
  16. return app.UseMiddleware<DefaultFilesMiddleware>(Options.Create(options));
  17. }
  18. }

紧接着查看DefaultFilesMiddlewareInvoke方法:

  1. public class DefaultFilesMiddleware
  2. {
  3. private readonly DefaultFilesOptions _options;
  4. private readonly PathString _matchUrl;
  5. private readonly RequestDelegate _next;
  6. private readonly IFileProvider _fileProvider;
  7. public DefaultFilesMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<DefaultFilesOptions> options)
  8. {
  9. _next = next;
  10. _options = options.Value;
  11. // 若未指定 FileProvider,则默认使用 hostingEnv.WebRootFileProvider
  12. _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
  13. _matchUrl = _options.RequestPath;
  14. }
  15. public Task Invoke(HttpContext context)
  16. {
  17. // 若已匹配到 Endpoint,则跳过
  18. // 若HTTP请求方法不是 Get,也不是 Head,则跳过
  19. // 如果请求路径不匹配,则跳过
  20. if (context.GetEndpoint() == null
  21. && Helpers.IsGetOrHeadMethod(context.Request.Method)
  22. && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath))
  23. {
  24. var dirContents = _fileProvider.GetDirectoryContents(subpath.Value);
  25. if (dirContents.Exists)
  26. {
  27. for (int matchIndex = 0; matchIndex < _options.DefaultFileNames.Count; matchIndex++)
  28. {
  29. string defaultFile = _options.DefaultFileNames[matchIndex];
  30. var file = _fileProvider.GetFileInfo(subpath.Value + defaultFile);
  31. // 找到了默认页
  32. if (file.Exists)
  33. {
  34. if (_options.RedirectToAppendTrailingSlash && !Helpers.PathEndsInSlash(context.Request.Path))
  35. {
  36. Helpers.RedirectToPathWithSlash(context);
  37. return Task.CompletedTask;
  38. }
  39. // 重写为默认页的Url,后续通过 StaticFileMiddleware 提供该页面
  40. context.Request.Path = new PathString(Helpers.GetPathValueWithSlash(context.Request.Path) + defaultFile);
  41. break;
  42. }
  43. }
  44. }
  45. }
  46. return _next(context);
  47. }
  48. }

FileServer

FileServer并不是某个具体的中间件,它的实现还是依赖了StaticFileMiddlewareDirectoryBrowserMiddlewareDefaultFilesMiddleware这3个中间件。不过,我们可以看一下UseFileServer里的逻辑:

  1. public static class FileServerExtensions
  2. {
  3. public static IApplicationBuilder UseFileServer(this IApplicationBuilder app)
  4. {
  5. return app.UseFileServer(new FileServerOptions());
  6. }
  7. public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, bool enableDirectoryBrowsing)
  8. {
  9. return app.UseFileServer(new FileServerOptions
  10. {
  11. EnableDirectoryBrowsing = enableDirectoryBrowsing
  12. });
  13. }
  14. public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, string requestPath)
  15. {
  16. return app.UseFileServer(new FileServerOptions
  17. {
  18. RequestPath = new PathString(requestPath)
  19. });
  20. }
  21. public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, FileServerOptions options)
  22. {
  23. // 启用默认页
  24. if (options.EnableDefaultFiles)
  25. {
  26. app.UseDefaultFiles(options.DefaultFilesOptions);
  27. }
  28. // 启用目录浏览
  29. if (options.EnableDirectoryBrowsing)
  30. {
  31. app.UseDirectoryBrowser(options.DirectoryBrowserOptions);
  32. }
  33. return app.UseStaticFiles(options.StaticFileOptions);
  34. }
  35. }

FileProvider in IWebHostingEnvironment

在接口IHostingEnvironment中,包含ContentRootFileProviderWebRootFileProvider两个文件提供程序。下面我们就看一下他们是如何被初始化的。

  1. internal class GenericWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider
  2. {
  3. private WebHostBuilderContext GetWebHostBuilderContext(HostBuilderContext context)
  4. {
  5. if (!context.Properties.TryGetValue(typeof(WebHostBuilderContext), out var contextVal))
  6. {
  7. var options = new WebHostOptions(context.Configuration, Assembly.GetEntryAssembly()?.GetName().Name);
  8. var webHostBuilderContext = new WebHostBuilderContext
  9. {
  10. Configuration = context.Configuration,
  11. HostingEnvironment = new HostingEnvironment(),
  12. };
  13. // 重点在这里,看这个 Initialize 方法
  14. webHostBuilderContext.HostingEnvironment.Initialize(context.HostingEnvironment.ContentRootPath, options);
  15. context.Properties[typeof(WebHostBuilderContext)] = webHostBuilderContext;
  16. context.Properties[typeof(WebHostOptions)] = options;
  17. return webHostBuilderContext;
  18. }
  19. var webHostContext = (WebHostBuilderContext)contextVal;
  20. webHostContext.Configuration = context.Configuration;
  21. return webHostContext;
  22. }
  23. }
  24. internal static class HostingEnvironmentExtensions
  25. {
  26. internal static void Initialize(this IWebHostEnvironment hostingEnvironment, string contentRootPath, WebHostOptions options)
  27. {
  28. hostingEnvironment.ApplicationName = options.ApplicationName;
  29. hostingEnvironment.ContentRootPath = contentRootPath;
  30. // 初始化 ContentRootFileProvider
  31. hostingEnvironment.ContentRootFileProvider = new PhysicalFileProvider(hostingEnvironment.ContentRootPath);
  32. var webRoot = options.WebRoot;
  33. if (webRoot == null)
  34. {
  35. // 如果 /wwwroot 目录存在,则设置为Web根目录
  36. var wwwroot = Path.Combine(hostingEnvironment.ContentRootPath, "wwwroot");
  37. if (Directory.Exists(wwwroot))
  38. {
  39. hostingEnvironment.WebRootPath = wwwroot;
  40. }
  41. }
  42. else
  43. {
  44. hostingEnvironment.WebRootPath = Path.Combine(hostingEnvironment.ContentRootPath, webRoot);
  45. }
  46. if (!string.IsNullOrEmpty(hostingEnvironment.WebRootPath))
  47. {
  48. hostingEnvironment.WebRootPath = Path.GetFullPath(hostingEnvironment.WebRootPath);
  49. if (!Directory.Exists(hostingEnvironment.WebRootPath))
  50. {
  51. Directory.CreateDirectory(hostingEnvironment.WebRootPath);
  52. }
  53. // 初始化 WebRootFileProvider
  54. hostingEnvironment.WebRootFileProvider = new PhysicalFileProvider(hostingEnvironment.WebRootPath);
  55. }
  56. else
  57. {
  58. hostingEnvironment.WebRootFileProvider = new NullFileProvider();
  59. }
  60. hostingEnvironment.EnvironmentName =
  61. options.Environment ??
  62. hostingEnvironment.EnvironmentName;
  63. }
  64. }

注意

  • 使用UseDirectoryBrowserUseStaticFiles提供文件浏览和访问时,URL 受大小写和基础文件系统字符的限制。例如,Windows 不区分大小写,但 macOS 和 Linux 区分大小写。
  • 如果使用 IIS 托管应用,那么 IIS 自带的静态文件处理器是不工作的,均是使用 ASP.NET Core Module 进行处理的,包括静态文件处理。

小结

  • 使用UseFileServer扩展方法提供文件浏览和访问,其集成了UseStaticFilesUseDirectoryBrowserUseDefaultFiles三个中间件的功能。

    • UseStaticFiles:注册StaticFilesMiddleware,提供文件访问
    • UseDirectoryBrowser:注册DirectoryBrowserMiddleware,提供文件目录浏览
    • UseDefaultFiles:注册DefaultFilesMiddleware,当Url未指定访问的文件名时,提供默认页。
  • 文件提供程序均实现了接口IFileProvider,常用的文件提供程序有以下三种:
    • PhysicalFileProvider:提供物理文件系统的访问
    • ManifestEmbeddedFileProvider:提供嵌入在程序集中的文件的访问
    • CompositeFileProvider:用于将多种文件提供程序进行集成。
  • 可通过IWebHostingEnvironment获取ContentRootFileProvider(默认目录为项目根目录)和WebRootFileProvider(默认目录为Web根目录)。

理解ASP.NET Core - 文件服务器(File Server)的更多相关文章

  1. 目录-理解ASP.NET Core

    <理解ASP.NET Core>基于.NET5进行整理,旨在帮助大家能够对ASP.NET Core框架有一个清晰的认识. 目录 [01] Startup [02] Middleware [ ...

  2. 理解ASP.NET Core - 基于JwtBearer的身份认证(Authentication)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 在开始之前,如果你还不了解基于Cookie的身份认证,那么建议你先阅读<基于Cookie ...

  3. 通过 Docker Compose 组合 ASP NET Core 和 SQL Server

    目录 Docker Compose 简介 安装 WebApi 项目 创建项目 编写Dockfile Web MVC 项目 创建项目 编写Dockfile 编写 docker-compose.yml文件 ...

  4. 理解 ASP.NET Core: 处理管道

    理解 ASP.NET Core 处理管道 在 ASP.NET Core 的管道处理部分,实现思想已经不是传统的面向对象模式,而是切换到了函数式编程模式.这导致代码的逻辑大大简化,但是,对于熟悉面向对象 ...

  5. 理解ASP.NET Core - [03] Dependency Injection

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 依赖注入 什么是依赖注入 简单说,就是将对象的创建和销毁工作交给DI容器来进行,调用方只需要接 ...

  6. 理解ASP.NET Core - [04] Host

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 本文会涉及部分 Host 相关的源码,并会附上 github 源码地址,不过为了降低篇幅,我会 ...

  7. 理解ASP.NET Core - 配置(Configuration)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 配置提供程序 在.NET中,配置是通过多种配置提供程序来提供的,包括以下几种: 文件配置提供程 ...

  8. 理解ASP.NET Core - 日志(Logging)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 快速上手 添加日志提供程序 在文章主机(Host)中,讲到Host.CreateDefault ...

  9. 理解ASP.NET Core - [01] Startup

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 准备工作:一份ASP.NET Core Web API应用程序 当我们来到一个陌生的环境,第一 ...

随机推荐

  1. 阿里云短信功能php

    1. 引入文件: https://help.aliyun.com/document_detail/53111.html?spm=a2c1g.8271268.10000.99.5a8ddf25gG0wW ...

  2. axios的简单的使用

    Axios 是什么? Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中. 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中).在服务端 ...

  3. ci框架驱动器

    1.驱动器什么是 驱动器是一种特殊类型的类库,它有一个父类和任意多个子类.子类可以访问父类, 但不能访问兄弟类.在你的控制器中,驱动器为你的类库提供了 一种优雅的语法,从而不用将它们拆成很多离散的类. ...

  4. Ybt#452-序列合并【期望dp】

    正题 题目链接:https://www.ybtoj.com.cn/contest/113/problem/2 题目大意 一个空序列,每次往末尾加入一个\([1,m]\)中的随机一个数.如果末尾两个数相 ...

  5. P3170-[CQOI2015]标识设计【插头dp】

    正题 题目链接:https://www.luogu.com.cn/problem/P3170 题目大意 给出\(n*m\)的网格上有一些障碍,要求用三个\(L\)形(高宽随意,不能退化成线段/点)覆盖 ...

  6. 华为云计算IE面试笔记-请描述华为容灾解决方案全景图,并解释双活数据中心需要从哪些角度着手考虑双活设计

    容灾全景图: 按照距离划分:分为本地容灾 同城容灾 异地容灾  本地容灾包括本地高可用和本地主备.(本数据中心的两机房.机柜) 本地高可用这个方案为了保持业务的连续性,从两个层面来考虑: ①一个是从主 ...

  7. 智汀家庭云-开发指南Golang:设备模块

    1.品牌 品牌指的是智能设备的品牌,SA通过插件的形式对该品牌下的设备进行发现控制.理论上来说一个品牌对应一个插件服务.您可以通过项目 根目录下的品牌查看SA支持的品牌.关于插件服务的详细信息可以参考 ...

  8. 3-等待线程终止的join方法

    等待线程终止的join方法 在项目实践中经常会遇到一个场景,就是需要等待某几件事完成之后才能继续往下执行,比如线程加载资源等等. package com.heiye.learn1; public cl ...

  9. CentOS7下Hadoop伪分布式环境搭建

    CentOS7下Hadoop伪分布式环境搭建 前期准备 1.配置hostname(可选,了解) 在CentOS中,有三种定义的主机名:静态的(static),瞬态的(transient),和灵活的(p ...

  10. 题解 CF241E Flights

    题目传送门 题目大意 给出一个 \(n\) 个点 \(m\) 条边的 \(\texttt{DAG}\) ,给每条边设定边权为 \(1\) 或者 \(2\) ,使得 \(1\to n\) 的每条路径长度 ...