1.使用
1.1 物理文件系统
1.1.1 依赖
Microsoft.Extensions.FileProviders.Physical
1.1.2 示例
(1). 创建 PhysicalFileProvider
using Microsoft.Extensions.FileProviders;
using System.IO;
// 指定物理目录(可以是绝对路径或相对路径)
var provider = new PhysicalFileProvider(Directory.GetCurrentDirectory());
// 获取文件信息
var fileInfo = provider.GetFileInfo("appsettings.json");
if (fileInfo.Exists)
{
using var stream = fileInfo.CreateReadStream();
using var reader = new StreamReader(stream);
string content = reader.ReadToEnd();
Console.WriteLine(content);
}
(2). 枚举目录内容
var directoryContents = provider.GetDirectoryContents("");
foreach (var item in directoryContents)
{
Console.WriteLine($"Name: {item.Name}, IsDirectory: {item.IsDirectory}");
}
(3). 监控文件变更
var changeToken = provider.Watch("*.json"); // 监听所有 JSON 文件变更
changeToken.RegisterChangeCallback(_ =>
{
Console.WriteLine("文件已变更!");
}, null);
// 保持程序运行以测试变更
Console.ReadLine();
(4). 在 ASP.NET Core 中使用
var builder = WebApplication.CreateBuilder(args);
// 自定义物理文件提供程序
var customProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "MyFiles"));
builder.Services.AddSingleton<IFileProvider>(customProvider);
var app = builder.Build();
// 提供静态文件(默认从 wwwroot 目录)
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = customProvider,
RequestPath = "/custom-files" // 访问路径映射
});
app.Run();
1.2 程序集(Assembly)中的嵌入式资源
核心作用
(1). 访问嵌入式资源
允许将编译时嵌入到程序集(.dll 或 .exe)中的资源文件(如 JSON、HTML、图片等)当作普通文件来读取,而无需直接使用 Assembly.GetManifestResourceStream。
(2). 与 IFileProvider 集成
提供 EmbeddedFileProvider 类,实现 IFileProvider 接口,使得嵌入式资源可以像物理文件一样被访问(如 GetFileInfo、GetDirectoryContents)。
(3). 支持 ASP.NET Core 静态文件中间件
可与 UseStaticFiles 结合,直接从程序集中提供静态文件(如前端库、插件资源等)。
1.2.1 依赖
# Microsoft.Extensions.FileProviders.Embedded
1.2.2 示例
(1). 配置嵌入式资源
在 .csproj 中启用嵌入式资源:
<ItemGroup>
<EmbeddedResource Include="Files\appsettings.json" />
<EmbeddedResource Include="wwwroot***" />
</ItemGroup>
(2). 访问嵌入式文件
using Microsoft.Extensions.FileProviders;
using System.Reflection;
// 创建 EmbeddedFileProvider,指定程序集
var assembly = Assembly.GetExecutingAssembly();
var embeddedProvider = new EmbeddedFileProvider(assembly);
// 获取文件信息
var fileInfo = embeddedProvider.GetFileInfo("Files/appsettings.json");
if (fileInfo.Exists)
{
using var stream = fileInfo.CreateReadStream();
using var reader = new StreamReader(stream);
string content = reader.ReadToEnd();
Console.WriteLine(content);
}
(3). 在 ASP.NET Core 中使用
var builder = WebApplication.CreateBuilder(args);
// 提供嵌入式静态文件(如 wwwroot 下的文件)
var embeddedProvider = new EmbeddedFileProvider(
Assembly.GetExecutingAssembly(),
"MyApp.wwwroot" // 资源根命名空间
);
builder.Services.AddSingleton<IFileProvider>(embeddedProvider);
var app = builder.Build();
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = embeddedProvider
});
app.Run();
1.3 复合文件系统
多个文件提供程序(IFileProvider)组合成一个逻辑上的单一文件提供程序
主要作用是将多个文件提供程序(IFileProvider)组合成一个逻辑上的单一文件提供程序,实现文件的聚合访问。
核心作用
- 合并多个文件源
允许将多个不同的IFileProvider(如物理文件系统、嵌入式资源、内存文件等)合并为一个统一的视图,客户端代码无需关心底层具体实现。 - 优先级覆盖
当多个文件提供程序包含同名文件时,第一个添加的提供程序中的文件会被优先返回(类似“覆盖”机制)。 - 统一访问接口
通过统一的IFileProvider接口访问多个来源的文件(如GetFileInfo、GetDirectoryContents等)。
1.3.1 依赖
Microsoft.Extensions.FileProviders.Composite
1.3.2 示例
using Microsoft.Extensions.FileProviders;
using System.IO;
// 创建多个文件提供程序
var physicalProvider = new PhysicalFileProvider(Directory.GetCurrentDirectory());
var embeddedProvider = new EmbeddedFileProvider(typeof(Program).Assembly);
// 组合它们(physicalProvider 的优先级更高)
var compositeProvider = new CompositeFileProvider(physicalProvider, embeddedProvider);
// 使用组合后的提供程序访问文件
var fileInfo = compositeProvider.GetFileInfo("appsettings.json");
if (fileInfo.Exists)
{
using var stream = fileInfo.CreateReadStream();
// 处理文件...
}
2. 文件系统整体结构
组合模式的实践
例举一个IChangeToken的实现,这个change token 基本上是对cancel token回调的包装,也是一次性的
public class ModelReloadToken : IChangeToken
{
private readonly CancellationTokenSource _cts;
public ModelReloadToken()
{
_cts = new CancellationTokenSource();
}
/// <summary>
/// Indicates if this token will proactively raise callbacks.
/// </summary>
public bool ActiveChangeCallbacks => true;
/// <summary>
/// Gets a value that indicates if a change has occurred.
/// </summary>
public bool HasChanged => _cts.IsCancellationRequested;
/// <summary>
/// Registers for a callback that will be invoked when the entry has changed. <see cref="Microsoft.Extensions.Primitives.IChangeToken.HasChanged"/>
/// MUST be set before the callback is invoked.
/// </summary>
/// <param name="callback">The callback to invoke.</param>
/// <param name="state">State to be passed into the callback.</param>
/// <returns>
/// An System.IDisposable that is used to unregister the callback.
/// </returns>
public IDisposable RegisterChangeCallback(Action<object> callback, object state) => _cts.Token.Register(callback, state);
/// <summary>
/// Used to trigger the change token when a reload occurs.
/// </summary>
public void OnReload() => _cts.Cancel();
}
ChangeToken类注册回调,具体怎么用问ai
public static class ChangeToken
{
/// <summary>
/// Registers the <paramref name="changeTokenConsumer"/> action to be called whenever the token produced changes.
/// </summary>
/// <param name="changeTokenProducer">Produces the change token.</param>
/// <param name="changeTokenConsumer">Action called when the token changes.</param>
/// <returns></returns>
public static IDisposable OnChange(Func<IChangeToken?> changeTokenProducer, Action changeTokenConsumer)
{
if (changeTokenProducer is null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.changeTokenProducer);
}
if (changeTokenConsumer is null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.changeTokenConsumer);
}
return new ChangeTokenRegistration<Action>(changeTokenProducer, callback => callback(), changeTokenConsumer);
}
/// <summary>
/// Registers the <paramref name="changeTokenConsumer"/> action to be called whenever the token produced changes.
/// </summary>
/// <param name="changeTokenProducer">Produces the change token.</param>
/// <param name="changeTokenConsumer">Action called when the token changes.</param>
/// <param name="state">state for the consumer.</param>
/// <returns></returns>
public static IDisposable OnChange<TState>(Func<IChangeToken?> changeTokenProducer, Action<TState> changeTokenConsumer, TState state)
{
if (changeTokenProducer is null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.changeTokenProducer);
}
if (changeTokenConsumer is null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.changeTokenConsumer);
}
return new ChangeTokenRegistration<TState>(changeTokenProducer, changeTokenConsumer, state);
}
private sealed class ChangeTokenRegistration<TState> : IDisposable
{
private readonly Func<IChangeToken?> _changeTokenProducer;
private readonly Action<TState> _changeTokenConsumer;
private readonly TState _state;
private IDisposable? _disposable;
private static readonly NoopDisposable _disposedSentinel = new NoopDisposable();
public ChangeTokenRegistration(Func<IChangeToken?> changeTokenProducer, Action<TState> changeTokenConsumer, TState state)
{
_changeTokenProducer = changeTokenProducer;
_changeTokenConsumer = changeTokenConsumer;
_state = state;
IChangeToken? token = changeTokenProducer();
RegisterChangeTokenCallback(token);
}
private void OnChangeTokenFired()
{
// The order here is important. We need to take the token and then apply our changes BEFORE
// registering. This prevents us from possible having two change updates to process concurrently.
//
// If the token changes after we take the token, then we'll process the update immediately upon
// registering the callback.
IChangeToken? token = _changeTokenProducer();
try
{
_changeTokenConsumer(_state);
}
finally
{
// We always want to ensure the callback is registered
RegisterChangeTokenCallback(token);
}
}
private void RegisterChangeTokenCallback(IChangeToken? token)
{
if (token is null)
{
return;
}
IDisposable registraton = token.RegisterChangeCallback(s => ((ChangeTokenRegistration<TState>?)s)!.OnChangeTokenFired(), this);
if (token.HasChanged && token.ActiveChangeCallbacks)
{
registraton?.Dispose();
return;
}
SetDisposable(registraton);
}
private void SetDisposable(IDisposable disposable)
{
// We don't want to transition from _disposedSentinel => anything since it's terminal
// but we want to allow going from previously assigned disposable, to another
// disposable.
IDisposable? current = Volatile.Read(ref _disposable);
// If Dispose was called, then immediately dispose the disposable
if (current == _disposedSentinel)
{
disposable.Dispose();
return;
}
// Otherwise, try to update the disposable
IDisposable? previous = Interlocked.CompareExchange(ref _disposable, disposable, current);
if (previous == _disposedSentinel)
{
// The subscription was disposed so we dispose immediately and return
disposable.Dispose();
}
else if (previous == current)
{
// We successfully assigned the _disposable field to disposable
}
else
{
// Sets can never overlap with other SetDisposable calls so we should never get into this situation
throw new InvalidOperationException("Somebody else set the _disposable field");
}
}
public void Dispose()
{
// If the previous value is disposable then dispose it, otherwise,
// now we've set the disposed sentinel
Interlocked.Exchange(ref _disposable, _disposedSentinel)?.Dispose();
}
private sealed class NoopDisposable : IDisposable
{
public void Dispose()
{
}
}
}
}