File Providers in ASP.NET Core
By Steve Smith
ASP.NET Core abstracts file system access through the use of File Providers.
View or download sample code ((xref:)how to download)
File Provider abstractions
File Providers are an abstraction over file systems. The main interface is IFileProvider
. IFileProvider
exposes methods to get file information (IFileInfo
), directory information (IDirectoryContents
), and to set up change notifications (using an IChangeToken
).
IFileInfo
provides methods and properties about individual files or directories. It has two boolean properties, Exists
and IsDirectory
, as well as properties describing the file’s Name
, Length
(in bytes), and LastModified
date. You can read from the file using its CreateReadStream
method.
File Provider implementations
Three implementations of IFileProvider
are available: Physical, Embedded, and Composite. The physical provider is used to access the actual system’s files. The embedded provider is used to access files embedded in assemblies. The composite provider is used to provide combined access to files and directories from one or more other providers.
PhysicalFileProvider
The PhysicalFileProvider
provides access to the physical file system. It wraps the System.IO.File
type (for the physical provider), scoping all paths to a directory and its children. This scoping limits access to a certain directory and its children, preventing access to the file system outside of this boundary. When instantiating this provider, you must provide it with a directory path, which serves as the base path for all requests made to this provider (and which restricts access outside of this path). In an ASP.NET Core app, you can instantiate a PhysicalFileProvider
provider directly, or you can request an IFileProvider
in a Controller or service’s constructor through dependency injection. The latter approach will typically yield a more flexible and testable solution.
The sample below shows how to create a PhysicalFileProvider
.
IFileProvider provider = new PhysicalFileProvider(applicationRoot);
IDirectoryContents contents = provider.GetDirectoryContents(""); // the applicationRoot contents
IFileInfo fileInfo = provider.GetFileInfo("wwwroot/js/site.js"); // a file under applicationRoot
You can iterate through its directory contents or get a specific file’s information by providing a subpath.
To request a provider from a controller, specify it in the controller’s constructor and assign it to a local field. Use the local instance from your action methods:
[!code-csharpMain]
1: using Microsoft.AspNetCore.Mvc;
2: using Microsoft.Extensions.FileProviders;
3:
4: namespace FileProviderSample.Controllers
5: {
6: public class HomeController : Controller
7: {
8: private readonly IFileProvider _fileProvider;
9:
10: public HomeController(IFileProvider fileProvider)
11: {
12: _fileProvider = fileProvider;
13: }
14:
15: public IActionResult Index()
16: {
17: var contents = _fileProvider.GetDirectoryContents("");
18: return View(contents);
19: }
20:
21: public IActionResult Error()
22: {
23: return View();
24: }
25: }
26: }
Then, create the provider in the app’s Startup
class:
[!code-csharpMain]
1: using System.Linq;
2: using System.Reflection;
3: using Microsoft.AspNetCore.Builder;
4: using Microsoft.AspNetCore.Hosting;
5: using Microsoft.Extensions.Configuration;
6: using Microsoft.Extensions.DependencyInjection;
7: using Microsoft.Extensions.FileProviders;
8: using Microsoft.Extensions.Logging;
9:
10: namespace FileProviderSample
11: {
12: public class Startup
13: {
14: private IHostingEnvironment _hostingEnvironment;
15: public Startup(IHostingEnvironment env)
16: {
17: var builder = new ConfigurationBuilder()
18: .SetBasePath(env.ContentRootPath)
19: .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
20: .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
21: .AddEnvironmentVariables();
22: Configuration = builder.Build();
23:
24: _hostingEnvironment = env;
25: }
26:
27: public IConfigurationRoot Configuration { get; }
28:
29: // This method gets called by the runtime. Use this method to add services to the container.
30: public void ConfigureServices(IServiceCollection services)
31: {
32: // Add framework services.
33: services.AddMvc();
34:
35: var physicalProvider = _hostingEnvironment.ContentRootFileProvider;
36: var embeddedProvider = new EmbeddedFileProvider(Assembly.GetEntryAssembly());
37: var compositeProvider = new CompositeFileProvider(physicalProvider, embeddedProvider);
38:
39: // choose one provider to use for the app and register it
40: //services.AddSingleton<IFileProvider>(physicalProvider);
41: //services.AddSingleton<IFileProvider>(embeddedProvider);
42: services.AddSingleton<IFileProvider>(compositeProvider);
43: }
44:
45: // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
46: public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
47: {
48: loggerFactory.AddConsole(Configuration.GetSection("Logging"));
49: loggerFactory.AddDebug();
50:
51: if (env.IsDevelopment())
52: {
53: app.UseDeveloperExceptionPage();
54: app.UseBrowserLink();
55: }
56: else
57: {
58: app.UseExceptionHandler("/Home/Error");
59: }
60:
61: app.UseStaticFiles();
62:
63: app.UseMvc(routes =>
64: {
65: routes.MapRoute(
66: name: "default",
67: template: "{controller=Home}/{action=Index}/{id?}");
68: });
69: }
70: }
71: }
In the Index.cshtml view, iterate through the IDirectoryContents
provided:
[!code-htmlMain]
1: @using Microsoft.Extensions.FileProviders
2: @model IDirectoryContents
3:
4: <h2>Folder Contents</h2>
5:
6: <ul>
7: @foreach (IFileInfo item in Model)
8: {
9: if (item.IsDirectory)
10: {
11: <li><strong>@item.Name</strong></li>
12: }
13: else
14: {
15: <li>@item.Name - @item.Length bytes</li>
16: }
17: }
18: </ul>
The result:
EmbeddedFileProvider
The EmbeddedFileProvider
is used to access files embedded in assemblies. In .NET Core, you embed files in an assembly with the <EmbeddedResource>
element in the .csproj file:
[!code-jsonMain]
1: <Project Sdk="Microsoft.NET.Sdk.Web">
2:
3: <PropertyGroup>
4: <TargetFramework>netcoreapp1.0</TargetFramework>
5: <PreserveCompilationContext>true</PreserveCompilationContext>
6: <AssemblyName>FileProviderSample</AssemblyName>
7: <OutputType>Exe</OutputType>
8: <PackageId>FileProviderSample</PackageId>
9: <RuntimeFrameworkVersion>1.0.3</RuntimeFrameworkVersion>
10: <PackageTargetFallback>$(PackageTargetFallback);dotnet5.6;portable-net45+win8</PackageTargetFallback>
11: </PropertyGroup>
12:
13: <ItemGroup>
14: <EmbeddedResource Include="Resource.txt;**\*.js" Exclude="bin\**;obj\**;**\*.xproj;packages\**;@(EmbeddedResource)" />
15: <Content Update="wwwroot\**\*;Views\**\*;Areas\**\Views;appsettings.json;web.config">
16: <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
17: </Content>
18: </ItemGroup>
19:
20: <ItemGroup>
21: <PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="1.0.1" />
22: <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.0.2" />
23: <PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="1.0.1" />
24: <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="1.0.2" />
25: <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="1.0.1" />
26: <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="1.0.1" />
27: <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="1.0.1" />
28: <PackageReference Include="Microsoft.Extensions.Logging" Version="1.0.1" />
29: <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="1.0.1" />
30: <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.0.1" />
31: <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="1.0.1" />
32: <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="1.0.1" />
33: <PackageReference Include="Microsoft.Extensions.FileProviders.Composite" Version="1.0.1" />
34: <PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink.Loader" Version="14.0.1" />
35: </ItemGroup>
36:
37: <Target Name="PrepublishScript" BeforeTargets="PrepareForPublish">
38: <Exec Command="bower install" />
39: <Exec Command="dotnet bundle" />
40: </Target>
41:
42: <ItemGroup>
43: <DotNetCliToolReference Include="BundlerMinifier.Core" Version="2.2.301" />
44: </ItemGroup>
45:
46: </Project>
You can use globbing patterns when specifying files to embed in the assembly. These patterns can be used to match one or more files.
[!NOTE] It’s unlikely you would ever want to actually embed every .js file in your project in its assembly; the above sample is for demo purposes only.
When creating an EmbeddedFileProvider
, pass the assembly it will read to its constructor.
The snippet above demonstrates how to create an EmbeddedFileProvider
with access to the currently executing assembly.
Updating the sample app to use an EmbeddedFileProvider
results in the following output:
[!NOTE] Embedded resources do not expose directories. Rather, the path to the resource (via its namespace) is embedded in its filename using
.
separators.
[!TIP] The
EmbeddedFileProvider
constructor accepts an optionalbaseNamespace
parameter. Specifying this will scope calls toGetDirectoryContents
to those resources under the provided namespace.
CompositeFileProvider
The CompositeFileProvider
combines IFileProvider
instances, exposing a single interface for working with files from multiple providers. When creating the CompositeFileProvider
, you pass one or more IFileProvider
instances to its constructor:
[!code-csharpMain]
1: using System.Linq;
2: using System.Reflection;
3: using Microsoft.AspNetCore.Builder;
4: using Microsoft.AspNetCore.Hosting;
5: using Microsoft.Extensions.Configuration;
6: using Microsoft.Extensions.DependencyInjection;
7: using Microsoft.Extensions.FileProviders;
8: using Microsoft.Extensions.Logging;
9:
10: namespace FileProviderSample
11: {
12: public class Startup
13: {
14: private IHostingEnvironment _hostingEnvironment;
15: public Startup(IHostingEnvironment env)
16: {
17: var builder = new ConfigurationBuilder()
18: .SetBasePath(env.ContentRootPath)
19: .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
20: .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
21: .AddEnvironmentVariables();
22: Configuration = builder.Build();
23:
24: _hostingEnvironment = env;
25: }
26:
27: public IConfigurationRoot Configuration { get; }
28:
29: // This method gets called by the runtime. Use this method to add services to the container.
30: public void ConfigureServices(IServiceCollection services)
31: {
32: // Add framework services.
33: services.AddMvc();
34:
35: var physicalProvider = _hostingEnvironment.ContentRootFileProvider;
36: var embeddedProvider = new EmbeddedFileProvider(Assembly.GetEntryAssembly());
37: var compositeProvider = new CompositeFileProvider(physicalProvider, embeddedProvider);
38:
39: // choose one provider to use for the app and register it
40: //services.AddSingleton<IFileProvider>(physicalProvider);
41: //services.AddSingleton<IFileProvider>(embeddedProvider);
42: services.AddSingleton<IFileProvider>(compositeProvider);
43: }
44:
45: // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
46: public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
47: {
48: loggerFactory.AddConsole(Configuration.GetSection("Logging"));
49: loggerFactory.AddDebug();
50:
51: if (env.IsDevelopment())
52: {
53: app.UseDeveloperExceptionPage();
54: app.UseBrowserLink();
55: }
56: else
57: {
58: app.UseExceptionHandler("/Home/Error");
59: }
60:
61: app.UseStaticFiles();
62:
63: app.UseMvc(routes =>
64: {
65: routes.MapRoute(
66: name: "default",
67: template: "{controller=Home}/{action=Index}/{id?}");
68: });
69: }
70: }
71: }
Updating the sample app to use a CompositeFileProvider
that includes both the physical and embedded providers configured previously, results in the following output:
Watching for changes
The IFileProvider
Watch
method provides a way to watch one or more files or directories for changes. This method accepts a path string, which can use globbing patterns to specify multiple files, and returns an IChangeToken
. This token exposes a HasChanged
property that can be inspected, and a RegisterChangeCallback
method that is called when changes are detected to the specified path string. Note that each change token only calls its associated callback in response to a single change. To enable constant monitoring, you can use a TaskCompletionSource
as shown below, or re-create IChangeToken
instances in response to changes.
In this article’s sample, a console application is configured to display a message whenever a text file is modified:
[!code-csharpMain]
1: using System;
2: using System.IO;
3: using System.Threading.Tasks;
4: using Microsoft.Extensions.FileProviders;
5: using Microsoft.Extensions.Primitives;
6:
7: namespace WatchConsole
8: {
9: public class Program
10: {
11: #region snippet1
12: private static PhysicalFileProvider _fileProvider =
13: new PhysicalFileProvider(Directory.GetCurrentDirectory());
14:
15: public static void Main(string[] args)
16: {
17: Console.WriteLine("Monitoring quotes.txt for changes (Ctrl-c to quit)...");
18:
19: while (true)
20: {
21: MainAsync().GetAwaiter().GetResult();
22: }
23: }
24:
25: private static async Task MainAsync()
26: {
27: IChangeToken token = _fileProvider.Watch("quotes.txt");
28: var tcs = new TaskCompletionSource<object>();
29:
30: token.RegisterChangeCallback(state =>
31: ((TaskCompletionSource<object>)state).TrySetResult(null), tcs);
32:
33: await tcs.Task.ConfigureAwait(false);
34:
35: Console.WriteLine("quotes.txt changed");
36: }
37: #endregion
38: }
39: }
The result, after saving the file several times:
[!NOTE] Some file systems, such as Docker containers and network shares, may not reliably send change notifications. Set the
DOTNET_USE_POLLINGFILEWATCHER
environment variable to1
ortrue
to poll the file system for changes every 4 seconds.
Globbing patterns
File system paths use wildcard patterns called globbing patterns. These simple patterns can be used to specify groups of files. The two wildcard characters are *
and **
.
*
Matches anything at the current folder level, or any filename, or any file extension. Matches are terminated by /
and .
characters in the file path.
**
Matches anything across multiple directory levels. Can be used to recursively match many files within a directory hierarchy.
Globbing pattern examples
directory/file.txt
Matches a specific file in a specific directory.
**directory/*.txt
**
Matches all files with .txt
extension in a specific directory.
directory/*/bower.json
Matches all bower.json
files in directories exactly one level below the directory
directory.
directory/**/*.txt
Matches all files with .txt
extension found anywhere under the directory
directory.
File Provider usage in ASP.NET Core
Several parts of ASP.NET Core utilize file providers. IHostingEnvironment
exposes the app’s content root and web root as IFileProvider
types. The static files middleware uses file providers to locate static files. Razor makes heavy use of IFileProvider
in locating views. Dotnet’s publish functionality uses file providers and globbing patterns to specify which files should be published.
Recommendations for use in apps
If your ASP.NET Core app requires file system access, you can request an instance of IFileProvider
through dependency injection, and then use its methods to perform the access, as shown in this sample. This allows you to configure the provider once, when the app starts up, and reduces the number of implementation types your app instantiates.
|