How to automatically embed static files in ASP.NET Core libraries and Razor libraries
asp.net-core razor-library c# embedded-files static-files embedded-static-files

How to automatically embed static files in ASP.NET Core libraries and Razor libraries

Have you ever dreamed of embedding everything required for a specific feature in a dedicated library? This article will teach you how to embed automatically your static files in an ASP.NET Core library. We have seen how to manually include static files in a razor library, this post will go even further and provide a way to do this automatically. Here you will find how to discover libraries that embed static files and include them automatically to serve them with the static file middleware.

In a previous post, we have seen how to manually include static files in a razor library, now we would improve this behavior and made it automatic.

We can do this in few steps:

  • find all libraries in the program,
  • filter relevant libraries,
  • discover embedded static files folders, and
  • register these folders to the static file middleware

We will treat these four steps in a different order.

How to find all libraries in the loaded program

The simple code AppDomain.CurrentDomain.GetAssemblies() returns all the loaded assemblies, however it returns only the loaded assemblies, meaning that if you have not yet used some of the assemblies your program requires, they are probably not loaded yet.

In order not to have surprises at runtime, we need to preload all assemblies that will need to be searched. Actually this has been already covered in a previous article that helps to understand how to dynamically pre-load assemblies of an ASP.NET Core program, so we may use the method LoadAllAssemblies written in this article to preload assemblies before any call.

Finding all libraries of the project may therefore be summarized as:

LoadAllAssemblies();
return AppDomain.CurrentDomain.GetAssemblies();

From this list of libraries, we need to filter the relevant libraries, meaning that we want to select all the libraries that embed static files.

Filter the relevant assemblies to look for embedded files

The following method will allow you to get all assemblies loaded for your program:

AppDomain.CurrentDomain.GetAssemblies()

However, you may be including 3rd party libraries that also embed some files, and may not want to expose their files if they are not intended to be, including some assemblies from the .Net Framework.

This part will explain a mechanism allowing to select the relevant assemblies.

For this we have two options:

  • specify a type of the desired assembly to target it directly,

    but this requires to know in advance which project will have to be sought and maintain a static list of them

  • specify a base type or class that needs to be implemented in the assembly

    this may seem a much simpler approach: just implement a maker interface in a project, then it will automatically be sought for embedded files

As we would like this to be as automatic as possible, we are going for the 2nd method, i.e. define a specific empty interface that will be hosted in a common project, and all projects that require to load some static files will just require to add a class implementing this interface.

Our goal is to return a list of relevant assemblies, our steps will be:

  • find all types in the project,
  • filter those which implement this interface,
  • select the types assemblies

How to find all types in the .NET Core project

The following simple code will list all types loaded for your program:

AppDomain.CurrentDomain
	// Get all assemblies loaded for the program
 	.GetAssemblies()
    // List all types of all assemblies and flattern them into an IEnumerable
    .SelectMany(s => s.GetTypes())

How to filter all types that implement a specific interface

Once you get the whole list of types of your program, you need to select those which implement your interface.

The following method extracts from a list of types those which implement a target type

/// <summary>
/// Get all types implementing the specified interface.
/// To ensure relevant assemblies, abstract classes are excluded from types
/// </summary>
/// <typeparam name="T">type of the targeted interface</typeparam>
/// <param name="types">list of types to be searced</param>
/// <returns>list of relevant types</returns>
public static IEnumerable<Type> GetAllTypesImplementingInterfaceT<T>(this IEnumerable<Type> types)
{
    var targetType = typeof(T);
    return types.Where(type2 => 
        // Check if the type implements the interface or is convertible to
        targetType.IsAssignableFrom(type2)
        // Ensure the list of the class interface contains the interface type
        && type2.GetInterfaces().Contains(targetType)
        // Ensure the type is not abstract and is actually a class, not another interface
        && !type2.IsAbstract && type2.IsClass)
        // Ensure unicity of results
        .Distinct();
}

Get the assemblies from the list of types

Getting the assembly list from the types is quite straightforward:

types.Select(t => t.Assembly).Distinct()

How to discover embedded static files paths

This section will explain how, from a specific assembly, extract all relevant embedded folders path.

In a previous article, we built a method that could be set as an extension method, in order to extract virtual folders that embed static files in a .Net Core assembly.

The following method will allow for a specific assembly to get all embedded wwwroot folders of the type /wwwroot or /areas/{areaname}/wwwroot in order to feed a ManifestEmbeddedFileProvider :

/// <summary>
/// Extract embed static files folders in a .Net Core assembly, 
/// when paths are of type:
/// /wwwroot
/// /Areas/AreaName/wwwroot
/// </summary>
static IEnumerable<(Assembly Assembly, string Path)> GetFilePathsFromAssembly(this Assembly a)
{
    // Extract paths of libs including embbeded resources of type:
    // /wwwroot
    // /Areas/AreaName/wwwroot
    // first we filter only files including a wwwroot name part
    return (from rn in a.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 (Assembly : a, Path : hasArea ? rootSubPath.Substring(1, rootSubPath.Length - 2) : "wwwroot" ))
            // we keep only one entry per folder
            .Distinct();
}

Register assemblies embedded files to the static file middleware

In a similar way as we have seen in the article how to manually include static files in a razor library, we will hack the WebRootFileProvider to serve in addition to disk files, embedded files using ManifestEmbeddedFileProvider provided by the ASP.NET Core framework.

The CompositeFileProvider can be used to aggregate two FileProvider. It can take as argument one or many FileProvider, to ensure the optimal performances, we will therefore create a single instance of this Composite provider and add all the relevant providers.

/// <summary>
/// Register assemblies embedded static files to the WebRootFileProvider used by the StaticFilesMiddleware
/// </summary>
/// <param name="env"></param>
/// <param name="filePaths"></param>
static void RegisterAssembliesStaticFilesFilePaths(this IHostingEnvironment env, IEnumerable<(Assembly Assembly, string Path)> filePaths)
{
    // Just output the embedded static files found, that will be served
    foreach (var f in filePaths)
        System.Diagnostics.Debug.WriteLine($"Included static files => {f.Assembly.GetName().FullName} {f.Path}");

    // Building a list of IFileProvider to feed a CompositeFileProvider
    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);
}

From there all we have to do is to use the StaticFileMiddleware, and you will reach the desired embedded files.

The order counts, so the original WebRootFileProvider is kept first (#1 priority), then the other are added in the order of discovery. An attribute could be added to the assembly to specify an order, but this will not be covered in this article.

Conclusion

We have seen all the steps required to add automatically embedded static files from different project libraries, here is the tool that we can build from this:

// https://dotnetstories.com
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;


namespace MD.Bases
{
    public static class LibraryStaticFilesExtensions
    {
        /// <summary>
        /// Register a single assembly's files
        /// Should be called before app.UseStaticFiles()
        /// </summary>
        /// <typeparam name="T">Any type in the targeted assembly</typeparam>
        /// <param name="env"></param>
        /// <param name="includeAreas">true if areas are defined and if the areas contain wwwroot folders</param>
        public static void UseLibraryStaticFiles<T>(this IHostingEnvironment env, bool includeAreas = false)
        {
            var assembly = typeof(T).Assembly;
            IEnumerable<(Assembly Assembly, string Path)> filePaths;

            if (!includeAreas)
                filePaths = new List<(Assembly,string)>() { (assembly, "wwwroot") };
            else
                filePaths = assembly.GetFilePathsFromAssembly();

            env.RegisterAssembliesStaticFilesFilePaths(filePaths);
        }

        /// <summary>
        /// Get all types implementing the specified interface.
        /// To ensure relevant assemblies, abstract classes are excluded from types
        /// </summary>
        /// <typeparam name="T">type of the targeted interface</typeparam>
        /// <param name="types">list of types to be searced</param>
        /// <returns>list of relevant types</returns>
        public static IEnumerable<Type> GetAllTypesImplementingInterfaceT<T>(this IEnumerable<Type> types)
        {
            var targetType = typeof(T);
            return types.Where(type2 => 
                // Check if the type implements the interface or is convertible to
                targetType.IsAssignableFrom(type2)
                // Ensure the list of the class interface contains the interface type
                && type2.GetInterfaces().Contains(targetType)
                // Ensure the type is not abstract and is actually a class, not another interface
                && !type2.IsAbstract && type2.IsClass)
                // Ensure unicity of results
                .Distinct();
        }


        /// <summary>
        /// Register all assemblies implementing
        /// </summary>
        /// <typeparam name="T">any interface that signals that the lib serves static files</typeparam>
        public static void UseLibsStaticFiles<T>(this IHostingEnvironment env, bool loadAssemblies = true)
        {
            var staticFileType = typeof(T);

            // Need to load dependencies to ensure having all references to the interface
            // (.net assemblies are not loaded)
            if (loadAssemblies) AssembliesTools.LoadAllAssemblies();

            // Looking for libraries having a class implementing the targeted interface
            var staticFileTypes = AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(s => s.GetTypes())
                .GetAllTypesImplementingInterfaceT<T>();

            env.UseLibsStaticFiles(staticFileTypes.ToArray());
        }

        /// <summary>
        /// Register several assemblies based on owned types
        /// </summary>
        public static void UseLibsStaticFiles(this IHostingEnvironment env, params Type[] assemblyTypes)
        {

            // Looking for libraries having a class implementing the targeted interface
            var staticFileAssemblies = assemblyTypes
                .Select(t => t.Assembly)
                .Distinct();

            // Extracts paths of libs including embbeded resources of type:
            // /wwwroot
            // /Areas/AreaName/wwwroot
            var filePaths = (from a in staticFileAssemblies
                             from a2 in a.GetFilePathsFromAssembly()
                             select a2).Distinct();

            env.RegisterAssembliesStaticFilesFilePaths(filePaths.Select(a => (a.Assembly, a.Path)));


        }

        /// <summary>
        /// Register assemblies embedded static files to the WebRootFileProvider used by the StaticFilesMiddleware
        /// </summary>
        /// <param name="env"></param>
        /// <param name="filePaths"></param>
        static void RegisterAssembliesStaticFilesFilePaths(this IHostingEnvironment env, IEnumerable<(Assembly Assembly, string Path)> filePaths)
        {
            foreach (var f in filePaths)
                System.Diagnostics.Debug.WriteLine($"Included static files => {f.Assembly.GetName().FullName} {f.Path}");

            // Building a list of IFileProvider to feed a CompositeFileProvider
            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);
        }

        static IEnumerable<(Assembly Assembly, string Path)> GetFilePathsFromAssembly(this Assembly a)
        {
            // Extract paths of libs including embbeded resources of type:
            // /wwwroot
            // /Areas/AreaName/wwwroot
            // first we filter only files including a wwwroot name part
            return (from rn in a.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 (Assembly : a, Path : hasArea ? rootSubPath.Substring(1, rootSubPath.Length - 2) : "wwwroot" ))
                    // we keep only one entry per folder
                    .Distinct();
        }

        public static void UseLibsStaticFiles(this IApplicationBuilder app)
            => app.UseLibsStaticFiles<IStaticFileLib>();

        public static void UseLibsStaticFiles<T>(this IApplicationBuilder app)
        {
            var env = app.ApplicationServices.GetRequiredService<IHostingEnvironment>();
            env.UseLibsStaticFiles<T>();
            app.UseStaticFiles();
        }
    }

    public static class AssembliesTools
    {
        private static void LoadAllAssemblies(bool includeFramework = false)
        {
            // Storage to ensure not loading the same assembly twice and optimize calls to GetAssemblies()
            ConcurrentDictionary<string, bool> Loaded = new ConcurrentDictionary<string, bool>();

            // Filter to avoid loading all the .net framework
            bool ShouldLoad(string assemblyName)
            {
                return (includeFramework || NotNetFramework(assemblyName))
                    && !Loaded.ContainsKey(assemblyName);
            }
            bool NotNetFramework(string assemblyName)
            {
                return !assemblyName.StartsWith("Microsoft.") && !assemblyName.StartsWith("System.")
                    && !assemblyName.StartsWith("Newtonsoft.") && assemblyName != "netstandard";
            }
            void LoadReferencedAssembly(Assembly assembly)
            {
                // Check all referenced assemblies of the specified assembly
                foreach (AssemblyName an in assembly.GetReferencedAssemblies().Where(a => ShouldLoad(a.FullName)))
                {
                    // Load the assembly and load its dependencies
                    LoadReferencedAssembly(Assembly.Load(an)); // AppDomain.CurrentDomain.Load(name)
                    Loaded.TryAdd(an.FullName, true);
                    System.Diagnostics.Debug.WriteLine($"\n>> REFERENCED ASSEMBLY => {an.FullName}");
                }
            }

            // Populate already loaded assemblies
            System.Diagnostics.Debug.WriteLine($">> Already loaded assemblies:");
            foreach (var a in AppDomain.CurrentDomain.GetAssemblies().Where(a => ShouldLoad(a.FullName)))
            {
                Loaded.TryAdd(a.FullName, true);
                System.Diagnostics.Debug.WriteLine($">>>> {a.FullName}");
            }
            int alreadyLoaded = Loaded.Keys.Count();
            System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();

            // Loop on loaded assemblies to load dependencies (it includes Startup assembly so should load all the dependency tree) 
            foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies().Where(a => NotNetFramework(a.FullName)))
                LoadReferencedAssembly(assembly);

            // Debug
            System.Diagnostics.Debug.WriteLine($"\n>> Assemblies loaded after scann ({(Loaded.Keys.Count - alreadyLoaded)} assemblies in {sw.ElapsedMilliseconds} ms):");
            foreach (var a in Loaded.Keys.OrderBy(k => k))
                System.Diagnostics.Debug.WriteLine($">>>> {a}");
        }
    }    


    // Implement this interface to include the assembly's embedded files
    public interface IStaticFileLib
    {
    }
}

How to use this tool

Usage, in Startup.cs, just replace app.UseStaticFiles() by:

app.UseLibsStaticFiles();

and implement the IStaticFileLib interface in all desired projects.

If you want to use a different interface than the one provided, just call app.UseLibsStaticFiles<T>() instead.

You can also use more than one type by calling env.UseLibsStaticFiles<T>(). This will stack the results of the desired types.


In this article, we have seen how to serve static files embedded in .net libraries automatically by the static file middleware. This method filters only files stored in wwwroot folders, but can of course be customized to serve files from specific folders.


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