How to Include static files in a Razor library
asp.net-core razor-library embedded-files static-files embedded-static-files wwwroot

How to Include static files in a Razor library

Razor libraries are a great improvement for splitting the development of ASP.NET Core projects into projects, however it still lacks (v2.1) the possibility to easily include static files in libraries, such as images, CSS or javascript files, fonts or web libraries. This posts explains the principle allowing to include static files in a Razor Library for folders of type /wwwroot or /Areas/Feature/wwwroot

Include wwwroot files in areas to be served as static files

It may be auto-implemented in future versions of the framework, however for now this does not seem to work as we could expect. This article will help you to get this working!

Static files are usually served by the StaticFileMiddleware which is added when calling app.UseStaticFiles().

We can find the source of this middleware on aspnetcore github repository

In this file, we can find the following line:

_fileProvider = _options.FileProvider ?? Helpers.ResolveFileProvider(hostingEnv);

And the helper actually returns hostingEnv.WebRootFileProvider. So we can choose among two options: either initializing the middleware with a custom FileProvider, either modify the environment default file provider which is not read-only.

As for my experience, it seems a better choice to go for the second option. Indeed we may require to read these static files in other parts of the application. It could for example be the use of LinkTagHelper that appends the static files version, which is using the same HostingEnvironment.WebRootFileProvider.

We are then going to enrich the default FileProvider used by the StaticFile middleware.

Following the insights from the Microsoft Documentation, we will use a ManifestEmbeddedFileProvider and a CompositeFileProvider to preserve the default behavior and add our custom file provider.

We have to follow several steps:

  1. set all files in wwwroot as EmbeddedResource
  2. include these resources in the library manifest
  3. serve these resources through the standard StaticFile middleware by modifying the WebRoot FileProvider

The standard library style of .net core makes the two first steps easily done just by modifying the .csproj file.

Set static files as EmbeddedResource

The following bloc will automatically set the status for all areas.

  <ItemGroup>
    <EmbeddedResource Include="Areas\*\wwwroot\**\*" />
  </ItemGroup>

Sometimes when adding/moving/copying files within the wwwroot folders, Visual Studio will add a <none Remove="myfile.css"/> line in the .csproj. If that happens, remove all non-necessary entries.

Include static files in the manifest

Including files in the manifest is simply done by adding the flag GenerateEmbeddedFilesManifest to the .csproj

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
  </PropertyGroup>

We may also require to add two NuGet packages in addition to Mvc:

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="2.2.0" />
    <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
  </ItemGroup>

Locate the relevant folders

The files are now well embedded in the DLL, the next step is then to locate the relevant folders and files in this DLL.

To specify the relevant folders, we are going to feed a list: List<string> filePaths;

Paths are not saved 'as is' in assemblies. As files are embedded files, they are referenced as AssemblyDefaultNamespace.(full path to file with '/' replaced by '.')

AssemblyDefaultNamespace is only a Visual Studio variable and is unfortunately not saved into the assembly, therefore it requires a little work to get to the files paths.

In case we would only like to include /wwwroot, the list is quite simple as it just contains this folder: filePaths = new List<string>() { "wwwroot" };

However if we want to include areas folders of type /Areas/feature/wwwroot, we have to do a little guess (or hardcode, but we don't like to). The solution I came up with is to browse all embedded files of the assembly and find when they are in a wwwroot folder, then extract the corresponding root path.

Using Linq, it gives the following piece of code:

// first we get the assembly in which we want to embedded the files
var assembly = typeof(TypeInFeatureAssembly).Assembly;

// we filter only files including a wwwroot name part
filePaths = (from rn in assembly.GetManifestResourceNames().Where(rnn => rnn.Contains(".wwwroot."))
        // then we check whether it is contained in an Area
    let hasArea = rn.Contains(".Areas.")
        // then we remove the path from wwwroot to the file name to keep the root
    let root = rn.Substring(0, rn.LastIndexOf(".wwwroot.") + ".wwwroot.".Length)
        // then we suppose that Areas is a root folder, so we remove 
        // the assembly default namespace
    let rootPath = !hasArea ? root : root.Substring(0, root.IndexOf(".Areas."))
        // we have a path of type .Areas.Feature.wwwroot. so we replace '.' with '/'
    let rootSubPath = !hasArea ? "" : root.Substring(root.IndexOf(".Areas.")).Replace('.', '/')
        // we trim the path of leading and tailing '/' 
    select  hasArea ? rootSubPath.Substring(1, rootSubPath.Length - 2) : "wwwroot" )
        // we keep only one entry per folder
    .Distinct().ToList();

Enrich the default WebRootFileProvider

Now that we have all files included in our assembly and generated the manifest, we need to make them available to the FileProvider used to serve static files.

That is where the dirty work really starts.

The step that we will be following are:

  1. Locate the assembly that needs to be loaded
  2. Locate the relevant folders
  3. Build FileProviders for these folders using ManifestEmbeddedFileProvider
  4. Concatenate these providers using a CompositeFileProvider

This would be the code to add to Startup.cs

var allProviders = new List<IFileProvider>();
// Keep the existing provider
allProviders.Add(env.WebRootFileProvider);
// Add a manifest provider for each relevant path
allProviders.AddRange(filePaths.Select(t => new ManifestEmbeddedFileProvider(t.Assembly, t.Path)));

// Replacing the previous WebRootFileProvider by our new composite one
env.WebRootFileProvider = new CompositeFileProvider(allProviders);

And well done, you can now serve static embedded files from your razor libraries!


We have seen how to serve embedded static files from a razor library, this method is re-usable and made automatic. This will be the subject of a future post!


Send
Please sign-in to comment
.X0001-01-01_00-00