My use case - I want to render the same template using different model instances from a Powershell script. I wrap Razorlight in an exe tool, but I do not want to call the tool with all the model objects at once - instead I want to be able to pass them as they become available.

This means I cannot use the MemoryCachingProvider. What I need is a file system caching provider and a way to reuse it between the calls to the tool.

Is there anything off-the-shelf?

EDIT 1

So I am banging the wall this whole day on this thing. I have the generated Assembly object, but it is not serializable. It is an in-memory assembly, so no file on disk. And there seems to be no way in RazorLight to instruct the compilation to save it on disk.

I must be missing something really obvious here, at least this is the sentiment I get from the first comment to this post. Please, put me out of my misery - share what am I missing here.

1

There are 1 best solutions below

0
mark On

So I failed to find a way to do it in the current version of RazorLight. I am extremely curious as to the "too many ways to approach this problem".

Anyway, my solution to the problem is to modify the RazorLight code to enable such a feature. Here is the PR - https://github.com/toddams/RazorLight/pull/492

If merged and released, then it would be possible to write a cache like this:

public class FileSystemCachingProvider : ICachingProvider
{
    private MemoryCachingProvider m_cache = new();
    private readonly string m_root;

    public FileSystemCachingProvider(string root)
    {
        m_root = root;
    }

    public void PrecreateAssemblyCallback(IGeneratedRazorTemplate generatedRazorTemplate, byte[] rawAssembly, byte[] rawSymbolStore)
    {
        var srcFilePath = Path.Combine(m_root, generatedRazorTemplate.TemplateKey[1..]);
        var asmFilePath = srcFilePath + ".dll";
        File.WriteAllBytes(asmFilePath, rawAssembly);
        if (rawSymbolStore != null)
        {
            var pdbFilePath = srcFilePath + ".pdb";
            File.WriteAllBytes(pdbFilePath, rawSymbolStore);
        }
    }

    public void CacheTemplate(string key, Func<ITemplatePage> pageFactory, IChangeToken expirationToken)
    {
    }

    public bool Contains(string key)
    {
        var srcFilePath = Path.Combine(m_root, key);
        var asmFilePath = srcFilePath + ".dll";
        if (File.Exists(asmFilePath))
        {
            var srcLastWriteTime = new FileInfo(srcFilePath).LastWriteTimeUtc;
            var asmLastWriteTime = new FileInfo(asmFilePath).LastWriteTimeUtc;
            return srcLastWriteTime < asmLastWriteTime;
        }
        return false;
    }

    public void Remove(string key)
    {
        var srcFilePath = Path.Combine(m_root, key);
        var asmFilePath = srcFilePath + ".dll";
        var pdbFilePath = srcFilePath + ".pdb";
        if (File.Exists(asmFilePath))
        {
            File.Delete(asmFilePath);
        }
        if (File.Exists(pdbFilePath))
        {
            File.Delete(pdbFilePath);
        }
    }

    public TemplateCacheLookupResult RetrieveTemplate(string key)
    {
        var srcFilePath = Path.Combine(m_root, key);
        var asmFilePath = srcFilePath + ".dll";
        if (File.Exists(asmFilePath))
        {
            var srcLastWriteTime = new FileInfo(srcFilePath).LastWriteTimeUtc;
            var asmLastWriteTime = new FileInfo(asmFilePath).LastWriteTimeUtc;
            if (srcLastWriteTime < asmLastWriteTime)
            {
                var res = m_cache.RetrieveTemplate(key);
                if (res.Success)
                {
                    return res;
                }
                var rawAssembly = File.ReadAllBytes(asmFilePath);
                var pdbFilePath = srcFilePath + ".pdb";
                var rawSymbolStore = File.Exists(pdbFilePath) ? File.ReadAllBytes(pdbFilePath) : null;
                return new TemplateCacheLookupResult(new TemplateCacheItem(key, CreateTemplatePage));

                ITemplatePage CreateTemplatePage()
                {
                    var templatePageTypes = Assembly
                        .Load(rawAssembly, rawSymbolStore)
                        .GetTypes()
                        .Where(t => typeof(ITemplatePage).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface)
                        .ToList();
                    if (templatePageTypes.Count != 1)
                    {
                        throw new ApplicationException($"Code bug: found {templatePageTypes.Count} concrete types implementing {nameof(ITemplatePage)} in the generated assembly.");
                    }
                    m_cache.CacheTemplate(key, CreateTemplatePage);
                    return CreateTemplatePage();

                    ITemplatePage CreateTemplatePage() => (ITemplatePage)Activator.CreateInstance(templatePageTypes[0]);
                }
            }
        }
        return new TemplateCacheLookupResult();
    }
}

And then one could use it like this:

string root = Path.GetDirectoryName(m_razorTemplateFilePath);
var provider = new FileSystemCachingProvider(root);
var engine = new RazorLightEngineBuilder()
    .UseFileSystemProject(root, "")
    .UseCachingProvider(provider)
    .AddPrecreateAssemblyCallbacks(provider.PrecreateAssemblyCallback)
    .Build();