1. ASP.NET Core 静态文件
在 ASP.NET Core 中,静态文件(如 HTML、CSS、图像和 JavaScript)默认存储在项目的 wwwroot 文件夹中。为了安全起见,框架不会自动提供这些文件,必须显式配置中间件。
1.1 启用静态文件服务
默认情况下,ASP.NET Core 不提供服务静态文件的功能。即使文件存在于 wwwroot 目录中,直接访问也会返回 404。需要在请求管道中调用 UseStaticFiles() 中间件。
步骤:
确保项目根目录下存在 wwwroot 文件夹。
在 Program.cs 中注册中间件。
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); // 启用默认 wwwroot 目录的静态文件服务 // 必须放在 UseRouting 之前或之后,但在 MapControllers/MapRazorPages 之前 app.UseStaticFiles(); app.MapGet("/", () => "Hello World!"); app.Run();1.2 新增静态文件目录
如果希望从 wwwroot 以外的目录提供静态文件(例如 MyStaticFiles),需要配置 StaticFileOptions 并指定 FileProvider。
步骤:
1.创建一个新的文件夹,例如 MyStaticFiles。
2. 在 Program.cs 中添加额外的 UseStaticFiles 调用。
using Microsoft.Extensions.FileProviders; using System.IO; var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); // 默认 wwwroot app.UseStaticFiles(); // 自定义目录:MyStaticFiles // 访问路径: /static/xxx.png var myStaticFilesPath = Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles"); if (Directory.Exists(myStaticFilesPath)) { app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(myStaticFilesPath), RequestPath = "/static" }); } app.Run();1.3 自定义 HTTP 响应头
可以通过 OnPrepareResponse 回调为静态文件设置自定义缓存策略或其他 HTTP 头。
步骤:
在 StaticFileOptions 中配置 OnPrepareResponse。
using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.FileProviders; // ... 在 Program.cs 中 app.UseStaticFiles(new StaticFileOptions { OnPrepareResponse = ctx => { // 设置缓存控制头,例如缓存 1 年 const int durationInSeconds = 60 * 60 * 24 * 365; ctx.Context.Response.Headers.Append( "Cache-Control", $"public, max-age={durationInSeconds}" ); // 添加自定义头 ctx.Context.Response.Headers.Append("X-Custom-Header", "MyValue"); } });1.4 静态文件的授权管理
默认情况下,UseStaticFiles 不提供授权检查。如果需要保护某些静态文件(如用户私有图片),可以使用 MapWhen 或自定义中间件,或者将文件放在受控区域并通过控制器动作流式传输。但更简单的方式是使用 UseStaticFiles 结合授权中间件的顺序,或者使用专门的端点映射。
对于简单的基于角色的访问,通常建议通过 Controller 行动作来提供受保护的文件,因为 UseStaticFiles 是管道早期的中间件,很难直接集成复杂的授权逻辑。但如果必须使用中间件方式,可以这样处理:
// 注意:这种方式较为粗糙,推荐对于敏感文件使用 Controller + Authorize Attribute app.Use(async (context, next) => { if (context.Request.Path.StartsWithSegments("/private")) { if (!context.User.Identity.IsAuthenticated) { context.Response.StatusCode = 401; return; } } await next(); }); // 然后配置 /private 路径的静态文件 app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "PrivateFiles")), RequestPath = "/private" });1.5 开启目录浏览服务
默认情况下,ASP.NET Core 禁止目录浏览以保障安全。如果需要列出目录内容,需启用 UseDirectoryBrowser。
步骤:
添加 UseDirectoryBrowser 中间件。
确保对应的 UseStaticFiles 也配置了相同的 FileProvider。
using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.FileProviders; // ... var dirPath = Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles"); if (Directory.Exists(dirPath)) { var fileProvider = new PhysicalFileProvider(dirPath); // 启用目录浏览 app.UseDirectoryBrowser(new DirectoryBrowserOptions { FileProvider = fileProvider, RequestPath = "/static" }); // 必须同时启用静态文件服务,否则点击文件无法下载/查看 app.UseStaticFiles(new StaticFileOptions { FileProvider = fileProvider, RequestPath = "/static" }); }1.6 自定义文件扩展名
ASP.NET Core 默认只识别常见的 MIME 类型。如果需要提供未知扩展名的文件(如 .xyz),需要配置 ContentTypeProvider。
步骤:
创建 FileExtensionContentTypeProvider 实例。
添加新的映射关系。
using Microsoft.AspNetCore.StaticFiles; // ... var provider = new FileExtensionContentTypeProvider(); // 添加自定义扩展名映射 provider.Mappings[".xyz"] = "application/x-custom-type"; provider.Mappings[".log"] = "text/plain"; app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = provider });2. 自定义一个简单的文件系统
ASP.NET Core 使用 IFileProvider 接口抽象文件系统。我们可以实现自定义的 IFileProvider 来从非物理存储(如 Redis、数据库、内存)提供文件。
2.1 文件和目录接口核心概念
要实现自定义文件系统,主要涉及三个接口:
IFileProvider: 入口点,用于获取 IDirectoryContents 和 IFileInfo。
IFileInfo: 描述单个文件的元数据(名称、长度、最后修改时间等)和内容流。
IDirectoryContents: 描述目录内容的集合。
2.2 RedisFileProvider 实现
以下是一个简化的示例,展示如何从 Redis 中读取文件内容。将文件内容存储在 Redis 字符串中,键名为文件路径。
安装 StackExchange.Redis NuGet 包。
RedisFileProvider
using Microsoft.Extensions.FileProviders; using StackExchange.Redis; using System; using System.Collections.Generic; using System.IO; using System.Threading; public class RedisFileProvider : IFileProvider { private readonly IDatabase _redisDb; private readonly string _keyPrefix; public RedisFileProvider(IConnectionMultiplexer redis, string keyPrefix = "files:") { _redisDb = redis.GetDatabase(); _keyPrefix = keyPrefix; } public IFileInfo GetFileInfo(string subpath) { if (string.IsNullOrEmpty(subpath) || subpath.Contains("..")) { return new NotFoundFileInfo(subpath); } // 清理路径,移除前导斜杠 var cleanPath = subpath.TrimStart('/'); var key = $"{_keyPrefix}{cleanPath}"; // 检查键是否存在 var exists = _redisDb.KeyExists(key); if (!exists) { return new NotFoundFileInfo(subpath); } // 获取文件大小和最后修改时间(这里简化处理,实际可能需要额外存储元数据) var value = _redisDb.StringGet(key); if (value.IsNullOrEmpty) { return new NotFoundFileInfo(subpath); } return new RedisFileInfo(subpath, value.Length, DateTime.UtcNow); } public IDirectoryContents GetDirectoryContents(string subpath) { // Redis 不是层级文件系统,模拟目录浏览非常复杂且低效 // 这里仅返回空集合,表示不支持目录浏览 return new NotFoundDirectoryContents(); } public IChangeToken Watch(string filter) { // 简化实现,返回不触发的 Token return NullChangeToken.Singleton; } } public class RedisFileInfo : IFileInfo { private readonly string _name; private readonly long _length; private readonly DateTimeOffset _lastModified; public RedisFileInfo(string name, long length, DateTimeOffset lastModified) { _name = name; _length = length; _lastModified = lastModified; } public bool Exists => true; public long Length => _length; public string PhysicalPath => null; // 无物理路径 public string Name => Path.GetFileName(_name); public DateTimeOffset LastModified => _lastModified; public bool IsDirectory => false; public Stream CreateReadStream() { // 在实际生产中,应该从 Redis 重新读取数据流 // 这里为了演示简化,假设数据已在构造函数中获取或需要重新查询 // 注意:IFileInfo 应该是轻量级的,CreateReadStream 才真正加载数据 // 由于 RedisFileInfo 构造时没有持有 byte[],这里需要访问 Redis // 为了保持类纯净,通常 RedisFileProvider 会在 GetFileInfo 时不加载内容, // 而在 CreateReadStream 时加载。 // 修正:由于 IFileInfo 接口限制,我们需要在 Provider 中注入 Redis 连接或在 FileInfo 中持有引用 // 此处为简化代码结构,假设我们通过某种方式能读取到流 // 在实际实现中,RedisFileInfo 应持有 IDatabase 引用和 Key throw new NotImplementedException("在实际实现中,此处应从 Redis 读取 Stream"); } } // 更完善的 RedisFileInfo 实现 public class RedisFileInfoComplete : IFileInfo { private readonly IDatabase _redisDb; private readonly string _key; private readonly string _name; public RedisFileInfoComplete(IDatabase redisDb, string key, string name) { _redisDb = redisDb; _key = key; _name = name; } public bool Exists => _redisDb.KeyExists(_key); public long Length => Exists ? _redisDb.StringLength(_key) : 0; public string PhysicalPath => null; public string Name => Path.GetFileName(_name); public DateTimeOffset LastModified => DateTimeOffset.UtcNow; // 简化 public bool IsDirectory => false; public Stream CreateReadStream() { var bytes = _redisDb.StringGet(_key); if (bytes.IsNullOrEmpty) { return Stream.Null; } return new MemoryStream(bytes.ToArray()); } }Program
using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using StackExchange.Redis; using System; using System.IO; var builder = WebApplication.CreateBuilder(args); // 配置 Redis 连接 (假设本地 Redis) var redis = ConnectionMultiplexer.Connect("localhost:6379"); builder.Services.AddSingleton<IConnectionMultiplexer>(redis); var app = builder.Build(); // 初始化一些测试数据到 Redis var db = redis.GetDatabase(); db.StringSet("files:test.txt", "Hello from Redis!"); db.StringSet("files/readme.md", "# Redis File Provider Demo"); // 使用自定义 RedisFileProvider // 注意:需要使用上面定义的 RedisFileInfoComplete 逻辑更新 RedisFileProvider // 这里为了代码可运行性,展示配置方式 /* * 由于上面的 RedisFileProvider 示例中 CreateReadStream 未完整实现, * 在实际项目中请确保 RedisFileInfo 能够访问 Redis 数据库以读取流。 */ // 假设我们已经完善了 RedisFileProvider 和 RedisFileInfoComplete // app.UseStaticFiles(new StaticFileOptions // { // FileProvider = new RedisFileProvider(redis), // RequestPath = "/redis-files" // }); app.MapGet("/", () => "Check /redis-files/test.txt (if configured correctly)"); app.Run(); using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using StackExchange.Redis; using System; using System.IO; var builder = WebApplication.CreateBuilder(args); // 配置 Redis 连接 (假设本地 Redis) var redis = ConnectionMultiplexer.Connect("localhost:6379"); builder.Services.AddSingleton<IConnectionMultiplexer>(redis); var app = builder.Build(); // 初始化一些测试数据到 Redis var db = redis.GetDatabase(); db.StringSet("files:test.txt", "Hello from Redis!"); db.StringSet("files/readme.md", "# Redis File Provider Demo"); // 使用自定义 RedisFileProvider // 注意:需要使用上面定义的 RedisFileInfoComplete 逻辑更新 RedisFileProvider // 这里为了代码可运行性,展示配置方式 /* * 由于上面的 RedisFileProvider 示例中 CreateReadStream 未完整实现, * 在实际项目中请确保 RedisFileInfo 能够访问 Redis 数据库以读取流。 */ // 假设我们已经完善了 RedisFileProvider 和 RedisFileInfoComplete // app.UseStaticFiles(new StaticFileOptions // { // FileProvider = new RedisFileProvider(redis), // RequestPath = "/redis-files" // }); app.MapGet("/", () => "Check /redis-files/test.txt (if configured correctly)"); app.Run();2.3 使用自定义文件系统
在 Program.cs 中,将自定义的 IFileProvider 传递给 UseStaticFiles 中间件。
- 依赖注入:可以将 IFileProvider 注册为服务,以便在应用的其他部分复用。
- 混合使用:可以同时使用多个 UseStaticFiles,一个用于物理文件(wwwroot),一个用于 Redis 文件。
- 性能考虑 :自定义 IFileProvider 中的 GetFileInfo 和 CreateReadStream 应尽量高效。对于 Redis,避免在 GetFileInfo 中加载大文件内容,仅在 CreateReadStream 中加载。
示例:
// 在 Program.cs 中 // 1. 获取 Redis 连接 var redis = ConnectionMultiplexer.Connect("localhost:6379"); // 2. 配置静态文件中间件使用 RedisProvider app.UseStaticFiles(new StaticFileOptions { FileProvider = new RedisFileProvider(redis), // 需使用包含完整逻辑的版本 RequestPath = "/cloud" }); // 现在访问 /cloud/test.txt 将从 Redis 读取 key "files:test.txt" 的内容