toddams / RazorLight

Template engine based on Microsoft's Razor parsing engine for .NET Core
Apache License 2.0
1.52k stars 259 forks source link

Implement the RazorLight.Precompile tool #492

Closed MarkKharitonov closed 1 year ago

MarkKharitonov commented 2 years ago

This method allows to implement a persistent cache as opposed to the existing in-memory cache.

I do not include an implementation of such a cache. But an example of how this can be done is:

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 it can be used 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();
MarkKharitonov commented 2 years ago

This Pull Request provides an answer to my question raised in the issue #491

MarkKharitonov commented 2 years ago

I have changed the PR to actually implement the RazorLight.Precompile tool. What do you think?

MarkKharitonov commented 2 years ago

I think it makes sense to abandon this PR and replace it with a new one. ?

MarkKharitonov commented 2 years ago

I have just added an ability to render the given model by the precompile command.

Please, run precompile -h and render -h to get help.

jzabroski commented 2 years ago

Hi Mark. I hope you are well. I have an immediate family member in the hospital so this has been on hold for now. I promise it's not simply being ignored

MarkKharitonov commented 2 years ago

@jzabroski - sorry to hear that. Hope things get better. Thanks for letting me know.

jzabroski commented 2 years ago

Getting to this now. Expect a reply in the next 3 days. I do think it looks good though

jzabroski commented 1 year ago

Thanks, Mark. Thank you as well for your patience while I worked through some things personally.

MarkKharitonov commented 1 year ago

No problem. Thank you for merging. Does it going to publish to nuget.org automatically?

jzabroski commented 1 year ago

No, I have to do that but I will do it now assuming I can remember how I set it up on GitHub & the API key is not expired. :-)

jzabroski commented 1 year ago

@MarkKharitonov It looks like there is one failing test after I merged. Can you check? I am not immediately clear why it happened but I think its due to the backslash separator is not compatible with Ubuntu Linux?

Path separator are platform dependent :

For windows, it’s \ and for unix it’s /.

  Failed RenderFolderRecursive("FullMessage.cshtml","folder\\MessageItem.cshtml",RazorLight.Caching.FileHashCachingStrategy) [93 ms]
  Error Message:
   RazorLight.RazorLightException : The razor template file /home/runner/work/RazorLight/RazorLight/tests/RazorLight.Precompile.Tests/bin/Debug/net6.0/Samples/folder\MessageItem.cshtml does not exist.
  Stack Trace:
MarkKharitonov commented 1 year ago

I will have a look

jzabroski commented 1 year ago

https://github.com/toddams/RazorLight/blob/c7f97d6d16091046e908b7764a712d69f12767a1/tests/RazorLight.Precompile.Tests/Render2Tests.cs#L58

I think you just need a platform check. We use xunit, which has some platform specific features built-in via plugins. But if the test is only for .NET 6, you can also just use the .NET 6 platform abstraction APIs to detect the platform elegantly.

jzabroski commented 1 year ago

Actually you can probably just use https://learn.microsoft.com/en-us/dotnet/api/system.io.path.pathseparator?view=netframework-4.8 since it works across all versions

MarkKharitonov commented 1 year ago

I need to setup a local linux environment to test, because it all passes on Windows.

jzabroski commented 1 year ago

I honestly dont think you need to go that far :-) You can fork GitHub repo and set-up actions locally in your repo using our template and just make the one line change i suggested to fix the issue. Have you ever just hit . on a repo after forking it? You get a VS code editor in your browser.

MarkKharitonov commented 1 year ago

I think there should be a PR workflow on this repository to prevent future inconveniences.

Anyway, I am able to run the workflow from my branch in my fork and I can see the problem. Will fix it.