Search code examples
c#dependency-injectionasp.net-core-5.0runtime-compilationfileprovider

Dependency inject a custom file provider for runtime compilation


I am experimenting with loading views from a database, and as suggested in the article one might want to add some caching to prevent hitting the database every time.

ConfigureServices:

services.AddHttpContextAccessor();
services.AddMemoryCache();

services.AddRazorPages()
    .AddRazorRuntimeCompilation(opt =>
    {
        opt.FileProviders.Add(new DatabaseFileProvider(Configuration["AppSettings:SQLConnectionString"]));
    });

DatabaseFileProvider constructor:

private string _connection;

public DatabaseFileProvider(string connection)
{
    _connection = connection;
}

How do I dependency inject an instance of IMemoryCache to the DatabaseFileProvider class?, as one can do with e.g. a singleton:

ConfigureServices:

services.AddSingleton<AppUtils>();

AppUtils constructor:

private static IMemoryCache _cache;

public AppUtils(IMemoryCache cache)
{
    _cache = cache;
}

Solution

  • Use DI services to configure MvcRazorRuntimeCompilationOptions directly

    Assuming a target provider like

    public class DatabaseFileProvider : IFileProvider {
        private string connection;
        private IMemoryCache cache;
    
        public DatabaseFileProvider(string connection, IMemoryCache cache) {
            this.connection = connection;
            this.cache = cache;
        }
    
        //...
    
    }
    

    Creating the provider with the aid of the DI services will allow for any registered dependencies to be resolved and explicitly injected using the deferred configuration delegate.

    Reference Use DI services to configure options

    services.AddHttpContextAccessor();
    services.AddMemoryCache();
    
    services
        .AddOptions<MvcRazorRuntimeCompilationOptions>() 
        .Configure<IServiceProvider>((options, sp) => { //<-- Configuration here
            var cs = Configuration["AppSettings:SQLConnectionString"]);
            var provider = ActivatorUtilities.CreateInstance<DatabaseFileProvider>(sp, cs);
            options.FileProviders.Add(provider);
        });
    
    services.AddRazorPages()
        .AddRazorRuntimeCompilation(); //remove configuration delegate here
    

    Configure allows the use of up to five services to configure options, but if a IServiceProvider is injected, the provider can be used in resolve more dependencies if needed.

    If that service locator approach is not preferred, the setup can be rearranged to follow a more pure DI design.

    services.AddHttpContextAccessor();
    services.AddMemoryCache();
    
    service.AddTransient<IFileProvider, DatabaseFileProvider>(sp => {
        var cs = Configuration["AppSettings:SQLConnectionString"]);
        var provider = ActivatorUtilities.CreateInstance<DatabaseFileProvider>(sp, cs);
        return provider;
    });
    
    //... register other providers if any
    
    services
        .AddOptions<MvcRazorRuntimeCompilationOptions>() 
        .Configure<IEnumerable<IFileProvider>>((options, providers) => {
            //add all registered providers
            foreach(IFileProvider provider in providers) {
                options.FileProviders.Add(provider);
            }
        });
    
    services.AddRazorPages()
        .AddRazorRuntimeCompilation();