Designing Modularity on ASP.NET Core: Virtual File System

Introduction

Creating a modular application is hard. Building a modular User Interface is even harder. You need to separately develop module pages, components but make them integrated and working together as like a monolithic application UI.

Creating such a modular architecture requires to build a strong infrastructure. This is what we are trying to do with the open source ABP framework project.

In this article, I will focus on the Virtual File System, an important part of the modular infrastructure and will explain why we need it and how it can be developed on top of ASP.NET Core MVC.

Video presentation of this article.

User Interface Components

A typical ASP.NET Core MVC web application UI consists of static and dynamic resources.

Static resources include JavaScript, CSS, Image… files. These resources are requested by browser and responded by the Static Files middleware. They are generally located under wwwroot folder of the application.

Dynamic resources are Razor views, pages and components. They are handled, compiled and rendered by the Razor View Engine.

Both static and dynamic files are normally located in the physical file system (while the latest ASP.NET Core has a pre-compile option, the main point is same).

User Interface Components in a Modular Application

In a modular application, UI components are distributed into modules and generally embedded into module assemblies (DLL files).

The Static File Middleware and the Razor View Engine can not work with the resources distributed across module assemblies.

The Virtual File System

The Virtual File System is an adapter (wrapper) that makes ASP.NET Core work with the resources located other than the physical file system.

Our virtual file system implementation can work with three type of file locations:

  • Embedded Files: The files located in a DLL as embedded resource. These resources are registered to the Virtual File System on the application startup.
  • Physical Files: The files located under the web application (wwwroot folder for static resources, root folder for views, pages… etc.). So, it’s backward compatible.
  • Dynamic Files: The files generated on runtime (such as dynamic js/css bundle files).

Dynamic files can override physical files and physical files can override embedded files (if located in the same path). In this way, your application can override UI components (like CSS files, JS files or views) of a module for customization purpose.

Virtual File Contribution

Modules should register/add their own embedded resources to the Virtual File System on the application startup. We have created VirtualFileSystemOptions for that purpose. Example usage:

context.Services.Configure<VirtualFileSystemOptions>(options =>
{
options
.FileSets
.AddEmbedded<MyModule>();
});

This code adds all embedded resources in the assembly of the MyModule class to the Virtual File System (VFS). Once all modules contributed to the VFS, we have a list of files and their paths (namespaces of an embedded is converted to a path) in an in-memory dictionary/collection.

IFileProvider Interface

ASP.NET Core uses IFileProvider interface to read files from the file system:

public interface IFileProvider
{
IFileInfo GetFileInfo(string subpath);
IDirectoryContents GetDirectoryContents(string subpath);
IChangeToken Watch(string filter);
}
  • GetFileInfo method is used to read a file info and content from a given path. It returns NotFoundFileInfo if given file does not exists.
  • GetDirectoryContents method is used to get list of files and directories inside a directory. It returns NotFoundDirectoryContents if given directory does not exists (can return singleton instance: NotFoundDirectoryContents.Singleton).
  • Watch method is used to get notified when a file or folder changes in the given path. filter can contain wildcards (like ‘*’).

We should implement this interface to return files from embedded/dynamic files. I will not share the implementation in this article, but you can guess it. If you want to know details, see our implementation and the documentation.

Configure Razor View Engine

Once we implement the Virtual File System, we can configure the RazorViewEngineOptions to add the new custom file provider:

context.Services.Configure<RazorViewEngineOptions>(options =>
{
options.FileProviders.Insert(0, new MyVirtualFileProvider());
});

Replace Static File Middleware

We normally use app.UseStaticFiles in order to serve physical files to browsers. We should also replace it by the Virtual File Provider. This part is also very easy. We can create such an extension method:

public static void UseVirtualFiles(this IApplicationBuilder app)
{
app.UseStaticFiles(
new StaticFileOptions
{
FileProvider = new MyVirtualFileProvider()
}
);
}

MyVirtualFileProvider is our example IFileProvider implementation. In your case, you can set FileProvider to any class that implements the IFileProvider interface as explained before.

Finally, we can use the UseVirtualFiles method instead of UseStaticFiles:

app.UseVirtualFiles();

Conclusion

I tried to briefly explain why we need to a Virtual File System to develop modular ASP.NET Core MVC applications and how to implement it.

I planned to write more articles on modular application development on ASP.NET Core based on my ABP framework development experiences.