Understanding ASP.NET Core - file server

Posted by pojr on Tue, 02 Nov 2021 01:43:36 +0100

Note: This article is part of the understanding ASP.NET Core series. Please check the top blog or Click here to view the full-text catalog

Provide static files

Static files are stored in the Web Root directory (Web Root) by default, and the path is the wwwroot folder under the project root directory (Content Root), that is, {Content Root}/wwwroot.

If you call the Host.CreateDefaultBuilder method, the current working directory (Directory.GetCurrentDirectory()) of the program will be set as the project root directory through the UseContentRoot method. You can view the details host A section.

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

Of course, you can also change the default path {Content Root}/wwwroot to a custom directory through the UseWebRoot extension method (but why do you change it?)

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            // The root directory of the configuration static resource is mywwwroot, and the default is wwwroot
            webBuilder.UseWebRoot("mywwwroot");

            webBuilder.UseStartup<Startup>();
        });

For convenience, wwwroot is used later to represent the Web root directory

First, we create a file named config.json in the wwwroot folder, and fill in the content casually

Note that make sure that the attribute of the file under wwwroot is copy if newer or always copy.

Next, we register the static file middleware StaticFileMiddleware through the UseStaticFiles extension method:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseStaticFiles();
}

Now, try to pass http://localhost:5000/config.json To get the contents of the wwwroot/config.json file

If SwaggerUI is enabled in your project, you will find that even if you do not manually add middleware by calling UseStaticFiles(), you can access the files under the wwwroot file, because SwaggerUIMiddleware uses StaticFileMiddleware

Provide files outside the Web root directory

Above, we have been able to provide the static files in the wwwroot folder. If our files are not in the wwwroot folder, how can we provide them?

Very simply, we can make some additional configurations for the StaticFileMiddleware middleware. Learn about the configuration items:

public abstract class SharedOptionsBase
{
    // Relative request path for custom static files
    public PathString RequestPath { get; set; }

    // File provider
    public IFileProvider FileProvider { get; set; }

    // Do you want to complete the slash "/" at the end of the path and redirect it
    public bool RedirectToAppendTrailingSlash { get; set; }
}

public class StaticFileOptions : SharedOptionsBase
{
    // ContentType provider
    public IContentTypeProvider ContentTypeProvider { get; set; }
    
    // If the ContentTypeProvider does not recognize the file type, do you still provide it as the default file type
    public bool ServeUnknownFileTypes { get; set; }
    
    // When serverunknownfiletypes = true, if an unrecognized file type occurs, the value of this attribute will be taken as the file type
    // When serverunknownfiletypes = true, this attribute must be assigned to take effect
    public string DefaultContentType { get; set; }
    
    // Whether to compress the file when the HTTP response compression middleware is registered
    public HttpsCompressionMode HttpsCompression { get; set; } = HttpsCompressionMode.Compress;
    
    // After setting the Status Code and Headers of the HTTP response, call before writing the Body
    // Use to add or change Headers
    public Action<StaticFileResponseContext> OnPrepareResponse { get; set; }
}

Suppose we now have such a file directory structure:

  • wwwroot
    • config.json
  • files
    • file.json

Then, in addition to the middleware for providing wwwroot static files, we also need to register a middleware for providing files static files:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // Provide wwwroot static file
    app.UseStaticFiles();

    // Provide files static files
    app.UseStaticFiles(new StaticFileOptions
    {
        FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files")),
        // Specify the access path of the file. It is allowed to have different names from the folder in the FileProvider
        // If not specified, you can use http://localhost:5000/file.json  obtain,
        // If specified, you need to pass http://localhost:5000/files/file.json  obtain
        RequestPath = "/files",
        OnPrepareResponse = ctx =>
        {
            // Configure the front-end cache for 600s (for the good operation of subsequent examples, it is recommended not to configure the Header first)
            ctx.Context.Response.Headers.Add(HeaderNames.CacheControl, "public,max-age=600");
        }
    });
}

It is recommended to place the publicly accessed files in the wwwroot directory and place the files authorized to be accessed to other directories (calling UseStaticFiles after calling UseAuthorization and specifying the file directory).

Provide directory browsing

Above, we can access the contents of a file through Url, and register DirectoryBrowserMiddleware through UseDirectoryBrowser, so that we can access the file list in the form of directory in the browser.

In addition, the configurable items of DirectoryBrowserMiddleware include a Formatter in addition to those in sharedoptions base, which is used to customize the directory view.

public class DirectoryBrowserOptions : SharedOptionsBase
{
    public IDirectoryFormatter Formatter { get; set; }
}

Examples are as follows:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDirectoryBrowser();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // Pass http://localhost:5000 , you can access the wwwroot directory
    app.UseDirectoryBrowser();
    
    // Pass http://localhost:5000/files , you can access the files directory
    app.UseDirectoryBrowser(new DirectoryBrowserOptions
    {
        // If you specify a file directory that is not provided in UseStaticFiles, you can browse the file list, but you cannot access the file content
        FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files")),
        // This must be consistent with the RequestPath in StaticFileOptions, otherwise the file will not be accessible
        RequestPath = "/files"
    });
}

Provide default page

Register the DefaultFilesMiddleware through UseDefaultFiles, which allows you to provide the display of default pages when accessing static files without providing a file name (that is, the path of a directory is passed in).

Note: UseDefaultFiles must be called before UseStaticFiles. Because DefaultFilesMiddleware is only responsible for rewriting URLs, in fact, the default page file is still provided through StaticFilesMiddleware.

By default, the middleware will search the HTML page files under the file directory in order:

  • default.htm
  • default.html
  • index.htm
  • index.html

In addition, in addition to the configurable items in SharedOptionsBase, the DefaultFileNames in the DefaultFilesMiddleware middleware also has a list, which is used to customize the file names of the default page. The default values in it are the four file names mentioned above.

public class DefaultFilesOptions : SharedOptionsBase
{
    public IList<string> DefaultFileNames { get; set; }
}

Examples are as follows:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // You will go to wwwroot to find the default.htm, default.html, index.htm, or index.html file as the default page
    app.UseDefaultFiles();

    // Set the default page for the files directory
    var defaultFilesOptions = new DefaultFilesOptions();
    defaultFilesOptions.DefaultFileNames.Clear();
    // Specifies the default page name
    defaultFilesOptions.DefaultFileNames.Add("index1.html");
    // Specify request path
    defaultFilesOptions.RequestPath = "/files";
    // Specify the directory where the default page is located
    defaultFilesOptions.FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files"));

    app.UseDefaultFiles(defaultFilesOptions);
}

UseFileServer

UseFileServer integrates the functions of UseStaticFiles, UseDefaultFiles and UseDirectoryBrowser, which is more convenient to use. It is also the preferred extension method used in our project.

Let's first look at fileserver options:

public class FileServerOptions : SharedOptionsBase
{
    public FileServerOptions()
        : base(new SharedOptions())
    {
        StaticFileOptions = new StaticFileOptions(SharedOptions);
        DirectoryBrowserOptions = new DirectoryBrowserOptions(SharedOptions);
        DefaultFilesOptions = new DefaultFilesOptions(SharedOptions);
        EnableDefaultFiles = true;
    }

    public StaticFileOptions StaticFileOptions { get; private set; }

    public DirectoryBrowserOptions DirectoryBrowserOptions { get; private set; }

    public DefaultFilesOptions DefaultFilesOptions { get; private set; }

    // Directory browsing is disabled by default
    public bool EnableDirectoryBrowsing { get; set; }

    // Enable default page by default (initialized in constructor)
    public bool EnableDefaultFiles { get; set; }
}

As you can see, fileserver options includes three options: StaticFileOptions, DirectoryBrowserOptions and DefaultFilesOptions, which can be customized for StaticFileMiddleware, DirectoryBrowserMiddleware and DefaultFilesMiddleware. In addition, static files and default pages are enabled by default, and directory browsing is disabled.

Here is an example:

Assumed file directory:

  • files
    • images
      • 1.jpg
    • file.json
    • myindex.html
public void ConfigureServices(IServiceCollection services)
{
    // If EnableDirectoryBrowsing is set to true, remember to register the service
    services.AddDirectoryBrowser();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{   
    // Enable StaticFileMiddleware
    // Enable DefaultFilesMiddleware
    // Disable DirectoryBrowserMiddleware
    // The default point is wwwroot
    app.UseFileServer();
    
    // Configuration for files folder
    var fileServerOptions = new FileServerOptions
    {
        FileProvider = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "files")),
        RequestPath = "/files",
        EnableDirectoryBrowsing = true
    };
    fileServerOptions.StaticFileOptions.OnPrepareResponse = ctx =>
    {
        // Configure cache for 600s
        ctx.Context.Response.Headers.Add(HeaderNames.CacheControl, "public,max-age=600");
    };
    fileServerOptions.DefaultFilesOptions.DefaultFileNames.Clear();
    fileServerOptions.DefaultFilesOptions.DefaultFileNames.Add("myindex.html");
    app.UseFileServer(fileServerOptions);
}

When visiting http://localhost:5000/files Because the file name myindex.html is added to DefaultFilesOptions.DefaultFileNames, the default page can be found and the content of the default page will be displayed.

If we don't add the file name myindex.html in DefaultFilesOptions.DefaultFileNames, we can't find the default page, but because DirectoryBrowsing is enabled, the file list will be displayed at this time.

Core configuration item

FileProvider

We have seen the PhysicalFileProvider above. It is just one of many file providers. All file providers implement IFileProvider interface:

public interface IFileProvider
{
    // Get the directory information of the given path, and you can enumerate all files in the directory
    IDirectoryContents GetDirectoryContents(string subpath);

    // Gets the file information for the given path
    IFileInfo GetFileInfo(string subpath);

    // Creates a ChangeToken for the specified filter
    IChangeToken Watch(string filter);
}

public interface IDirectoryContents : IEnumerable<IFileInfo>, IEnumerable
{
    bool Exists { get; }
}

public interface IFileInfo
{
    bool Exists { get; }

    bool IsDirectory { get; } 

    DateTimeOffset LastModified { get; }

    // Byte length
    // - 1 if the directory or file does not exist
    long Length { get; }

    // Directory or file name, pure file name, excluding path
    string Name { get; }

    // File path, including file name
    // Returns null if the file is not directly accessible
    string PhysicalPath { get; }

    // Create a read-only stream of this file
    Stream CreateReadStream();
}

There are three common file providers:

  • PhysicalFileProvider
  • ManifestEmbeddedFileProvider
  • CompositeFileProvider

glob mode

Before introducing these three file providers, let's talk about glob mode, that is, wildcard mode. The two wildcards are * and * *.

  • *: matches any content, any file name or any file extension under the current directory level (excluding subdirectories), which can be separated by /, \ and.
  • **: matches any content of a multi-level directory (including subdirectories), which is used to recursively match multiple files of a multi-level directory.

PhysicalFileProvider

The PhysicalFileProvider is used to provide access to the physical file system. The provider needs to limit the file path range to a directory and its subdirectories, and cannot access the contents outside the directory.

When instantiating the file provider, you need to provide an absolute directory path as the root of the file directory.

glob (wildcard) mode is not supported for PhysicalFileProvider directory or file path.

ManifestEmbeddedFileProvider

The ManifestEmbeddedFileProvider is used to provide access to files embedded in an assembly.

You may be unfamiliar with this embedded file. It doesn't matter. Please follow the following steps:

  • Install Nuget package: install package microsoft.extensions.fileproviders.embedded
  • To edit a. csproj file:
    • Add < generateembeddedfilesmanifest >, and set it to true
    • Use < embeddedresource > to add files to embed

The following is an example of a. csproj file:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="5.0.11" />
  </ItemGroup>

  <ItemGroup>
    <EmbeddedResource Include="files\**" />
  </ItemGroup>
</Project>

Now we provide access to the files embedded in the files directory of the assembly through the ManifestEmbeddedFileProvider:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
     var fileServerOptions = new FileServerOptions();
    fileServerOptions.StaticFileOptions.FileProvider = new ManifestEmbeddedFileProvider(Assembly.GetExecutingAssembly(), "/files");
    fileServerOptions.StaticFileOptions.RequestPath = "/files";

    app.UseFileServer(fileServerOptions);
}

Now, you can pass http://localhost:5000/files/file.json To access the file.

CompositeFileProvider

CompositeFileProvider is used to integrate multiple file providers.

For example:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    var fileServerOptions = new FileServerOptions();
    var fileProvider = new CompositeFileProvider(
        env.WebRootFileProvider,
        new ManifestEmbeddedFileProvider(Assembly.GetExecutingAssembly(), "/files")
    );
    fileServerOptions.StaticFileOptions.FileProvider = fileProvider;
    fileServerOptions.StaticFileOptions.RequestPath = "/composite";

    app.UseFileServer(fileServerOptions);
}

Now, you can pass http://localhost:5000/composite/file.json To access the file.

ContentTypeProvider

You must be familiar with the content type in the Http request header. The ContentTypeProvider is used to provide the mapping relationship between file extension and MIME type.

If we do not display the specified ContentTypeProvider, the framework uses FileExtensionContentTypeProvider by default, which implements the interface IContentTypeProvider:

public interface IContentTypeProvider
{
    // Try to get the corresponding MIME type according to the file path
    bool TryGetContentType(string subpath, out string contentType);
}

public class FileExtensionContentTypeProvider : IContentTypeProvider
{
    public FileExtensionContentTypeProvider()
        : this(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            // ... omit 10000 words here
        }
    {
    }

    public FileExtensionContentTypeProvider(IDictionary<string, string> mapping)
    {
        Mappings = mapping;
    }

    public IDictionary<string, string> Mappings { get; private set; }

    public bool TryGetContentType(string subpath, out string contentType)
    {
        string extension = GetExtension(subpath);
        if (extension == null)
        {
            contentType = null;
            return false;
        }
        return Mappings.TryGetValue(extension, out contentType);
    }

    private static string GetExtension(string path)
    {
        // The reason why Path.GetExtension() is not used is that when there are invalid characters in the path, it will throw an exception, which should not be thrown here.

        if (string.IsNullOrWhiteSpace(path))
        {
            return null;
        }

        int index = path.LastIndexOf('.');
        if (index < 0)
        {
            return null;
        }

        return path.Substring(index);
    }
}

In the parameterless constructor of FileExtensionContentTypeProvider, 380 known file extension and MIME type Mappings are added by default and stored in the Mappings property. You can also add custom Mappings or remove unwanted Mappings.

Core middleware

StaticFileMiddleware

Using the UseStaticFiles extension method, you can easily register StaticFileMiddleware:

public static class StaticFileExtensions
{
    public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app)
    {
        return app.UseMiddleware<StaticFileMiddleware>();
    }
    
    public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, string requestPath)
    {
        return app.UseStaticFiles(new StaticFileOptions
        {
            RequestPath = new PathString(requestPath)
        });
    }

    public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app, StaticFileOptions options)
    {
        return app.UseMiddleware<StaticFileMiddleware>(Options.Create(options));
    }
}

Next, check the Invoke method of StaticFileMiddleware:

public class StaticFileMiddleware
{
    private readonly StaticFileOptions _options;
    private readonly PathString _matchUrl;
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;
    private readonly IFileProvider _fileProvider;
    private readonly IContentTypeProvider _contentTypeProvider;

    public StaticFileMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<StaticFileOptions> options, ILoggerFactory loggerFactory)
    {
        _next = next;
        _options = options.Value;
        // If no ContentTypeProvider is specified, FileExtensionContentTypeProvider is used by default
        _contentTypeProvider = _options.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
        // If no FileProvider is specified, hostingEnv.WebRootFileProvider is used by default
        _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
        _matchUrl = _options.RequestPath;
        _logger = loggerFactory.CreateLogger<StaticFileMiddleware>();
    }

    public Task Invoke(HttpContext context)
    {
        // If the Endpoint has been matched, skip
        if (!ValidateNoEndpoint(context))
        {
            _logger.EndpointMatched();
        }
        // If the HTTP request method is neither Get nor Head, skip
        else if (!ValidateMethod(context))
        {
            _logger.RequestMethodNotSupported(context.Request.Method);
        }
        // If the request paths do not match, skip
        else if (!ValidatePath(context, _matchUrl, out var subPath))
        {
            _logger.PathMismatch(subPath);
        }
        // Skip if ContentType is not supported
        else if (!LookupContentType(_contentTypeProvider, _options, subPath, out var contentType))
        {
            _logger.FileTypeNotSupported(subPath);
        }
        else
        {
            // Attempt to provide a static file
            return TryServeStaticFile(context, contentType, subPath);
        }

        return _next(context);
    }

    private static bool ValidateNoEndpoint(HttpContext context) => context.GetEndpoint() == null;

    private static bool ValidateMethod(HttpContext context) => Helpers.IsGetOrHeadMethod(context.Request.Method);

    internal static bool ValidatePath(HttpContext context, PathString matchUrl, out PathString subPath) => Helpers.TryMatchPath(context, matchUrl, forDirectory: false, out subPath);

    internal static bool LookupContentType(IContentTypeProvider contentTypeProvider, StaticFileOptions options, PathString subPath, out string contentType)
    {
        // Check whether the ContentType is supported in the Provider
        if (contentTypeProvider.TryGetContentType(subPath.Value, out contentType))
        {
            return true;
        }

        // If an unknown file type is provided, it is set as the default ContentType
        if (options.ServeUnknownFileTypes)
        {
            contentType = options.DefaultContentType;
            return true;
        }

        return false;
    }

    private Task TryServeStaticFile(HttpContext context, string contentType, PathString subPath)
    {
        var fileContext = new StaticFileContext(context, _options, _logger, _fileProvider, contentType, subPath);

        // Skip if file does not exist
        if (!fileContext.LookupFileInfo())
        {
            _logger.FileNotFound(fileContext.SubPath);
        }
        else
        {
            // If the file exists, the static file is provided
            return fileContext.ServeStaticFile(context, _next);
        }

        return _next(context);
    }
}

DirectoryBrowserMiddleware

The DirectoryBrowserMiddleware can be easily registered through the UseDirectoryBrowser extension method:

public static class DirectoryBrowserExtensions
{
    public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app)
    {
        return app.UseMiddleware<DirectoryBrowserMiddleware>();
    }

    public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, string requestPath)
    {
        return app.UseDirectoryBrowser(new DirectoryBrowserOptions
        {
            RequestPath = new PathString(requestPath)
        });
    }

    public static IApplicationBuilder UseDirectoryBrowser(this IApplicationBuilder app, DirectoryBrowserOptions options)
    {
        return app.UseMiddleware<DirectoryBrowserMiddleware>(Options.Create(options));
    }
}

Next, check the Invoke method of DirectoryBrowserMiddleware:

public class DirectoryBrowserMiddleware
{
    private readonly DirectoryBrowserOptions _options;
    private readonly PathString _matchUrl;
    private readonly RequestDelegate _next;
    private readonly IDirectoryFormatter _formatter;
    private readonly IFileProvider _fileProvider;

    public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<DirectoryBrowserOptions> options)
        : this(next, hostingEnv, HtmlEncoder.Default, options)
    {
    }

    public DirectoryBrowserMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, HtmlEncoder encoder, IOptions<DirectoryBrowserOptions> options)
    {
        _next = next;
        _options = options.Value;
        // If no FileProvider is specified, hostingEnv.WebRootFileProvider is used by default
        _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
        _formatter = _options.Formatter ?? new HtmlDirectoryFormatter(encoder);
        _matchUrl = _options.RequestPath;
    }

    public Task Invoke(HttpContext context)
    {
        // If the Endpoint has been matched, skip
        // If the HTTP request method is neither Get nor Head, skip
        // If the request paths do not match, skip
        // Skip if file directory does not exist
        if (context.GetEndpoint() == null
            && Helpers.IsGetOrHeadMethod(context.Request.Method)
            && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath)
            && TryGetDirectoryInfo(subpath, out var contents))
        {
            if (_options.RedirectToAppendTrailingSlash && !Helpers.PathEndsInSlash(context.Request.Path))
            {
                Helpers.RedirectToPathWithSlash(context);
                return Task.CompletedTask;
            }

            // Generate file browse view
            return _formatter.GenerateContentAsync(context, contents);
        }

        return _next(context);
    }

    private bool TryGetDirectoryInfo(PathString subpath, out IDirectoryContents contents)
    {
        contents = _fileProvider.GetDirectoryContents(subpath.Value);
        return contents.Exists;
    }
}

DefaultFilesMiddleware

The DefaultFilesMiddleware can be easily registered through the UseDefaultFiles extension method:

public static class DefaultFilesExtensions
{
    public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app)
    {
        return app.UseMiddleware<DefaultFilesMiddleware>();
    }

    public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, string requestPath)
    {
        return app.UseDefaultFiles(new DefaultFilesOptions
        {
            RequestPath = new PathString(requestPath)
        });
    }

    public static IApplicationBuilder UseDefaultFiles(this IApplicationBuilder app, DefaultFilesOptions options)
    {
        return app.UseMiddleware<DefaultFilesMiddleware>(Options.Create(options));
    }
}

Next, check the Invoke method of DefaultFilesMiddleware:

public class DefaultFilesMiddleware
{
    private readonly DefaultFilesOptions _options;
    private readonly PathString _matchUrl;
    private readonly RequestDelegate _next;
    private readonly IFileProvider _fileProvider;

    public DefaultFilesMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, IOptions<DefaultFilesOptions> options)
    {
        _next = next;
        _options = options.Value;
        // If no FileProvider is specified, hostingEnv.WebRootFileProvider is used by default
        _fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);
        _matchUrl = _options.RequestPath;
    }
    
    public Task Invoke(HttpContext context)
    {
        // If the Endpoint has been matched, skip
        // If the HTTP request method is neither Get nor Head, skip
        // If the request paths do not match, skip
        if (context.GetEndpoint() == null
            && Helpers.IsGetOrHeadMethod(context.Request.Method)
            && Helpers.TryMatchPath(context, _matchUrl, forDirectory: true, subpath: out var subpath))
        {
            var dirContents = _fileProvider.GetDirectoryContents(subpath.Value);
            if (dirContents.Exists)
            {
                for (int matchIndex = 0; matchIndex < _options.DefaultFileNames.Count; matchIndex++)
                {
                    string defaultFile = _options.DefaultFileNames[matchIndex];
                    var file = _fileProvider.GetFileInfo(subpath.Value + defaultFile);
                    
                    // Default page found
                    if (file.Exists)
                    {
                        if (_options.RedirectToAppendTrailingSlash && !Helpers.PathEndsInSlash(context.Request.Path))
                        {
                            Helpers.RedirectToPathWithSlash(context);
                            return Task.CompletedTask;
                        }
                        
                        // Rewrite to the Url of the default page, and then provide the page through StaticFileMiddleware
                        context.Request.Path = new PathString(Helpers.GetPathValueWithSlash(context.Request.Path) + defaultFile);
                        break;
                    }
                }
            }
        }

        return _next(context);
    }
}

FileServer

FileServer is not a specific middleware. Its implementation still depends on three middleware: static filemiddleware, DirectoryBrowserMiddleware and DefaultFilesMiddleware. However, we can look at the logic in UseFileServer:

public static class FileServerExtensions
{
    public static IApplicationBuilder UseFileServer(this IApplicationBuilder app)
    {
        return app.UseFileServer(new FileServerOptions());
    }

    public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, bool enableDirectoryBrowsing)
    {
        return app.UseFileServer(new FileServerOptions
        {
            EnableDirectoryBrowsing = enableDirectoryBrowsing
        });
    }

    public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, string requestPath)
    {
        return app.UseFileServer(new FileServerOptions
        {
            RequestPath = new PathString(requestPath)
        });
    }

    public static IApplicationBuilder UseFileServer(this IApplicationBuilder app, FileServerOptions options)
    {
        // Enable default page
        if (options.EnableDefaultFiles)
        {
            app.UseDefaultFiles(options.DefaultFilesOptions);
        }

        // Enable directory browsing
        if (options.EnableDirectoryBrowsing)
        {
            app.UseDirectoryBrowser(options.DirectoryBrowserOptions);
        }

        return app.UseStaticFiles(options.StaticFileOptions);
    }
}

FileProvider in IWebHostingEnvironment

The interface IHostingEnvironment contains two file providers: ContentRootFileProvider and WebRootFileProvider. Let's take a look at how they are initialized.

internal class GenericWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider
{
    private WebHostBuilderContext GetWebHostBuilderContext(HostBuilderContext context)
    {
        if (!context.Properties.TryGetValue(typeof(WebHostBuilderContext), out var contextVal))
        {
            var options = new WebHostOptions(context.Configuration, Assembly.GetEntryAssembly()?.GetName().Name);
            var webHostBuilderContext = new WebHostBuilderContext
            {
                Configuration = context.Configuration,
                HostingEnvironment = new HostingEnvironment(),
            };
            
            // Here's the point. Look at the Initialize method
            webHostBuilderContext.HostingEnvironment.Initialize(context.HostingEnvironment.ContentRootPath, options);
            context.Properties[typeof(WebHostBuilderContext)] = webHostBuilderContext;
            context.Properties[typeof(WebHostOptions)] = options;
            return webHostBuilderContext;
        }

        var webHostContext = (WebHostBuilderContext)contextVal;
        webHostContext.Configuration = context.Configuration;
        return webHostContext;
    }
}

internal static class HostingEnvironmentExtensions
{
    internal static void Initialize(this IWebHostEnvironment hostingEnvironment, string contentRootPath, WebHostOptions options)
    {
        hostingEnvironment.ApplicationName = options.ApplicationName;
        hostingEnvironment.ContentRootPath = contentRootPath;
        // Initialize ContentRootFileProvider
        hostingEnvironment.ContentRootFileProvider = new PhysicalFileProvider(hostingEnvironment.ContentRootPath);

        var webRoot = options.WebRoot;
        if (webRoot == null)
        {
            // If the / wwwroot directory exists, it is set to the Web root directory
            var wwwroot = Path.Combine(hostingEnvironment.ContentRootPath, "wwwroot");
            if (Directory.Exists(wwwroot))
            {
                hostingEnvironment.WebRootPath = wwwroot;
            }
        }
        else
        {
            hostingEnvironment.WebRootPath = Path.Combine(hostingEnvironment.ContentRootPath, webRoot);
        }

        if (!string.IsNullOrEmpty(hostingEnvironment.WebRootPath))
        {
            hostingEnvironment.WebRootPath = Path.GetFullPath(hostingEnvironment.WebRootPath);
            if (!Directory.Exists(hostingEnvironment.WebRootPath))
            {
                Directory.CreateDirectory(hostingEnvironment.WebRootPath);
            }
            
            // Initialize WebRootFileProvider
            hostingEnvironment.WebRootFileProvider = new PhysicalFileProvider(hostingEnvironment.WebRootPath);
        }
        else
        {
            hostingEnvironment.WebRootFileProvider = new NullFileProvider();
        }

        hostingEnvironment.EnvironmentName =
            options.Environment ??
            hostingEnvironment.EnvironmentName;
    }
}

be careful

  • When using UseDirectoryBrowser and usestatic files to provide file browsing and access, URL s are limited by case and underlying file system characters. For example, Windows is not case sensitive, but Mac OS and Linux are case sensitive.
  • If IIS is used to host applications, the static file processor provided with IIS does not work and is processed using the ASP.NET Core Module, including static file processing.

Summary

  • Use the UseFileServer extension method to provide file browsing and access, which integrates the functions of three middleware: usestatic files, UseDirectoryBrowser and UseDefaultFiles.
    • UseStaticFiles: register StaticFilesMiddleware to provide file access
    • UseDirectoryBrowser: register DirectoryBrowserMiddleware and provide file directory browsing
    • UseDefaultFiles: register DefaultFilesMiddleware. When the Url does not specify the file name to be accessed, the default page is provided.
  • All file providers implement the interface IFileProvider. There are three common file providers:
    • PhysicalFileProvider: provides access to the physical file system
    • ManifestEmbeddedFileProvider: provides access to files embedded in an assembly
    • CompositeFileProvider: used to integrate multiple file providers.
  • ContentRootFileProvider (the default directory is the project root directory) and WebRootFileProvider (the default directory is the Web root directory) can be obtained through IWebHostingEnvironment.

Topics: ASP.NET .NET