注:本文隶属于《理解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
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根目录)。

理解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中的数组分页实现(非数据库)

    在日常开发的业务环境中,我们一般都会使用 MySQL 语句来实现分页的功能.但是,往往也有些数据并不多,或者只是获取 PHP 中定义的一些数组数据时需要分页的功能.这时,我们其实不需要每次都去查询数据 ...

  2. PHP的Mcrypt加密扩展知识了解

    今天我们来学习的是 PHP 中的一个过时的扩展 Mcrypt .在 PHP7 之前,这个扩展是随 PHP 安装包一起内置发布的,但是现在新版本的 PHP 中已经没有了,需要使用这个扩展的话我们需要单独 ...

  3. php CURL 发送http请求 GET POST

    * CURL http://www.php.net/manual/en/book.curl.php http://jp2.php.net/manual/en/function.curl-setopt. ...

  4. django 内置用户-装饰器

    """ 一.如何给python内置用户添加额外的字段,注意一定义在没有迁移数据之前定义,否则会报错 1.在models中先调用 from django.contrib.a ...

  5. 2021牛客暑期多校训练营9C-Cells【LGV引理,范德蒙德行列式】

    正题 题目链接:https://ac.nowcoder.com/acm/contest/11260/C 题目大意 一个平面上,\(n\)个起点\((0,a_i)\)分别对应终点\((i,0)\),每次 ...

  6. P5956-[POI2017]Podzielno【数学】

    正题 题目链接:https://www.luogu.com.cn/problem/P5956 题目大意 \(B\)进制下,给出序列\(a\),\(a_i\)表示数字\(i\)有多少个.求一个最大的\( ...

  7. 【Go】Golang实现gRPC的Proxy的原理

    背景 gRPC是Google开始的一个RPC服务框架, 是英文全名为Google Remote Procedure Call的简称. 广泛的应用在有RPC场景的业务系统中,一些架构中将gRPC请求都经 ...

  8. Java编程思想 第九章 接口

    第九章 接口 抽象类和抽象方法 抽象:从具体事物抽出.概括出它们共同的方面.本质属性与关系等,而将个别的.非本质的方面.属性与关系舍弃,这种思维过程,称为抽象. 这句话概括了抽象的概念,而在Java中 ...

  9. The art of multipropcessor programming 读书笔记-硬件基础1

    本系列是 The art of multipropcessor programming 的读书笔记,在原版图书的基础上,结合 OpenJDK 11 以上的版本的代码进行理解和实现.并根据个人的查资料以 ...

  10. 反调试——7——CRC检测

    反调试--7--CRC检测 CRC32: CRC的全称是循环冗余校验,作用是为了检测数据的完整性. CRC32的检测原理: 程序被编译后,代码段是固定的,因为已经被写死了. 我们在调试程序的时候,打断 ...