1. ASP.NET Core 静态文件
在 ASP.NET Core 中,静态文件(如 HTML、CSS、图像和 JavaScript)默认存储在项目的 wwwroot 文件夹中。为了安全起见,框架不会自动提供这些文件,必须显式配置中间件。
1.1 启用静态文件服务
默认情况下,ASP.NET Core 不提供服务静态文件的功能。即使文件存在于 wwwroot 目录中,直接访问也会返回 404。需要在请求管道中调用 UseStaticFiles() 中间件。
步骤:
确保项目根目录下存在 wwwroot 文件夹。
在 Program.cs 中注册中间件。
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。
- 在 Program.cs 中添加额外的 UseStaticFiles 调用。
cs
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。
cs
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 是管道早期的中间件,很难直接集成复杂的授权逻辑。但如果必须使用中间件方式,可以这样处理:
cs
// 注意:这种方式较为粗糙,推荐对于敏感文件使用 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。
cs
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 实例。
添加新的映射关系。
cs
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
cs
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
cs
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 中加载。
示例:
cs
// 在 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" 的内容