ASP.NET Core 文件系统

108 阅读4分钟

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 接口,使得嵌入式资源可以像物理文件一样被访问(如 GetFileInfoGetDirectoryContents)。
(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)组合成一个逻辑上的单一文件提供程序,实现文件的聚合访问。

核心作用

  1. 合并多个文件源
    允许将多个不同的 IFileProvider(如物理文件系统、嵌入式资源、内存文件等)合并为一个统一的视图,客户端代码无需关心底层具体实现。
  2. 优先级覆盖
    当多个文件提供程序包含同名文件时,第一个添加的提供程序中的文件会被优先返回(类似“覆盖”机制)。
  3. 统一访问接口
    通过统一的 IFileProvider 接口访问多个来源的文件(如 GetFileInfoGetDirectoryContents 等)。

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()
                {
                }
            }
        }
    }