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

With 2.0.0-beta1 how to use templates from memory without setting a project? #250

Closed RedX2501 closed 4 years ago

RedX2501 commented 5 years ago

Is your feature request related to a problem? Please describe. Currently a ???Project must be passed to the builder to be able to render templates even when using CompileRenderStringAsync.

Describe the solution you'd like It would be nice if nothing was needed if the templates I'm using are self-contained.

jzabroski commented 4 years ago

@RedX2501 What is a self-contained template? Do you mean passing in a Stream object that resolves to a cshtml file? What is the interface contract you want?

mattwendels commented 4 years ago

@jzabroski You used to be able to write:

var engine = new RazorLightEngineBuilder() .UseMemoryCachingProvider() .Build();

.. but this now throws an exception, saying the _razorLightProject cannot be null.

jzabroski commented 4 years ago

Which release was that behavior available in? I'm newer to RazorLight than others, but trying to help make it great.

I imagine the reason for the change is so that it more explicitly wires dependencies.

Did the old behavior just assume embedded resources, file system, or some other approach I'm not thinking of, like not using a key at all and instead passing in a whole cshtml file as a string?

mattwendels commented 4 years ago

I'm not sure to be honest. In the version I currently use (just trying to upgrade now) - we did:

var razorEngine = new RazorLightEngineBuilder().UseMemoryCachingProvider().Build();

... and then specified the entire template contents as a string:

var template = File.ReadAllText(razorTemplatePath);

var emailBody = razorEngine.CompileRenderAsync(razorTemplateCacheKey, template, viewModel).Result;

Looking at it, I think to get it working now I think instead of reading the file myself, I think I need to specifiy the directory containing the template via the .UseFileSystemProject(<template dir>) method and then just pass the template file name in to the .CompileRenderStringAsync() method...

I think?

jzabroski commented 4 years ago

Correct. I would recommend using embedded resources, though. The main reason to use file system is to allow hot swapping templates, but you lose control over your deployment artifacts when you do that. I don't trust engineers to not sneak on servers and change things. At companies I've been at, sometimes it's business analysts who are friends with the executives who get access and change things and then bring down a whole sub-section of your app.

tl;dr: Use EmbeddedResource Project instead of UseFileSystemProject

mattwendels commented 4 years ago

Thanks for the clarification. Much appreciated!

mattwendels commented 4 years ago

Sorry, just following up again here. I think there's a bit of design flaw here. In 2.0.0-beta1, you MUST specify a project type when creating the engine:

var razorEngine = new RazorLightEngineBuilder()
                .UseEmbeddedResourcesProject(typeof(EmailService)) // exception without this (or another project type)
                .UseMemoryCachingProvider()
                .Build();

But you then you can call razorEngine.CompileRenderStringAsync() and just pass a string in as your template, not using the resources provider at all.

I think just rendering a template from a string is a pretty common use-case, and seems confusing that you need to specify a project type (e.g. resources) that ultimately you'll never use.

Is there any scope to reintroduct the ability create a RazorEngine instance without a project type?

jzabroski commented 4 years ago

I mean, if you want to write test cases and the feature to support it, you are welcome to. But I suspect it's a lot of work to make sure there are no regressions, because everywhere you have a _razorProjectSystem you have a chance for a regression.

W.r.t using UseEmbeddedResourcesProject, the pattern Ivan uses in his tests is he creates a dummy class called Root that is in the Root namespace for your project. Not a great solution, but it's what I copied in my own projects.

mattwendels commented 4 years ago

Yeah, I was looking at the source earlier and I agree!

You're right though you can get around it by just specifying .UseEmbeddedResourcesProject(typeof() and then just use the CompileRenderStringAsync() without issue.

Hopefully this solves @RedX2501 original question.

Thanks again

RedX2501 commented 4 years ago

I'm quite glad this issue received so much attention :)

I'd really like to know WHAT a project is. Why I need it for and what are the implications of setting one? Is this in some documentation that I've missed? I don't have an ASP.Net background and the documentation about Razor that I've found never talk about projects.

allow hot swapping templates, but you lose control over your deployment artifacts when you do that

This is exactly my use-case. I have a model which the users can access with their own templates to generate C++ code for their application.

By self-contained I mean that the template has no imports to additional files.

Hello @user. I'm a self contained template because I don't require anything else.

mattwendels commented 4 years ago

As far as I can tell a project is effectively the ‘source’ of your .cshtml templates. E.g a folder somewhere or an embedded resource etc.

But my point above is that we have a method on the RazorEngine that just lets you compile a template by passing it in as a string (CompileRenderStringAsync). The ‘project’ (or source) is not relevant, so why do we need to set it when creating the RazorEngine?

Looking at the source the project is used everywhere and it seems to have become enforced during a big redesign of the compilation process. I suspect this particular use-case was maybe forgotten about?

Anyway, as far as the implications go, I just set RazorEngine to use the embedded resource project, passing in the type of the current class, but carried on calling CompileRenderStringAsync() anyway and passing in a string as the template and everything seems to work fine.

The resource project is totally ignored as far as I can tell and I haven’t seen any issues (yet).

jzabroski commented 4 years ago

@RedX2501 Interesting, it would be cool to create a list in the README of projects using RazorLight and their use cases. I got involved because I want my project FluentMigrator to use it for customizing SQL templates. FluentMigrator is arguably the most powerful SQL abstraction tool out there for modifying db schemas. But a pain point exists around SQL-level overrides ; core templates cannot be trivially overridden.

RedX2501 commented 4 years ago

Sorry, just following up again here. I think there's a bit of design flaw here. In 2.0.0-beta1, you MUST specify a project type when creating the engine:

var razorEngine = new RazorLightEngineBuilder()
                .UseEmbeddedResourcesProject(typeof(EmailService)) // exception without this (or another project type)
                .UseMemoryCachingProvider()
                .Build();

Excuse my stupidity but I just compiled 2.0.0-beta1 and I can't seem to find the RazorLightEngineBuilder class. Where is it?

I can't seem to find it anywhere with the available source code for beta 1:

image

RedX2501 commented 4 years ago

I'm absolutely confused now :( I installed version 2.0.0-beta1 from the nuget and voila. A RazorLightEngineBuilder is available. Yet in the source I can't find any trace of it...

Well if I use version 2.0.0-beta1 I get the following exception:

image

jzabroski commented 4 years ago

You need a German Delta Layer on RazorLight to prevent it from leaking 😆 https://www.youtube.com/watch?v=ZbQmKsXIBYc

jzabroski commented 4 years ago

100 jahre langzeitbestandigkeit

RedX2501 commented 4 years ago

Master (60fdee0b937233d201f8c57410e6ee5a02b2dffc) seems to work at least.

RedX2501 commented 4 years ago

I think we can close this as it seems to work now.

Maybe it would be interesting to retag 2.0.0-beta1 to the proper commit that was used to generate the nuget package...

vijkumarsharma commented 2 years ago

@jzabroski @mattwendels Can you please suggest to me what is the issue here

    public async Task<string> GenerateHtml(EmailModel model)
    {
        var engine = new RazorLightEngineBuilder()
       .UseFileSystemProject("C:/Users/vijku/source/repos/CalendarApp/CalendarApp/bin/Release/net6.0/win-x64/publish/win-x64")
       .UseMemoryCachingProvider()
       .Build();
        string result = await engine.CompileRenderAsync("Templates/Email.cshtml", model);

        return result;

    }

    I am getting the below error :

image

jzabroski commented 2 years ago

DefaultMetadataReferenceManager uses specific paths that may not be supported by your deployment option. This can happen if you compile everything into a giant dll where the CodeBase property is null. You'll have to implement an alternative if you stick with whatever deployment you are using that is causing this

vijkumarsharma commented 2 years ago

@jzabroski thanks for the help . I was able to fix the issue . Now i am getting below issue

Code used:

public async Task GenerateHtml(EmailModel model) { var engine = new RazorLightEngineBuilder() .UseEmbeddedResourcesProject(typeof(EmailModel)) .EnableDebugMode(true) .UseMemoryCachingProvider() .Build(); string result = await engine.CompileRenderAsync("Email", model);

        return result;

Error coming
RazorLight.TemplateNotFoundException: RazorLightProjectItem of type RazorLight.Razor.EmbeddedRazorProjectItem with key Email.cshtml could not be found by the RazorLightProject of type RazorLight.Razor.EmbeddedRazorProject and does not exist in dynamic templates. See the "KnownDynamicTemplateKeys" and "KnownProjectTemplateKeys" properties for known template keys.
     at RazorLight.Compilation.RazorTemplateCompiler.CreateRuntimeCompilationWorkItem(String templateKey)
     at RazorLight.Compilation.RazorTemplateCompiler.OnCacheMissAsync(String templateKey)
     at RazorLight.EngineHandler.CompileTemplateAsync(String key)
     at RazorLight.EngineHandler.CompileRenderAsync[T](String key, T model, ExpandoObject viewBag)
     at CalendarAppService.EmailSender.GenerateHtml(EmailModel model) in C:\Users\vijku\source\repos\CalendarApp\CalendarApp\CalendarApp.cs:line 179
     at CalendarAppService.EmailSender.Main() in C:\Users\vijku\source\repos\CalendarApp\CalendarApp\CalendarApp.cs:line 47
     at CalendarApp.Worker.ExecuteAsync(CancellationToken stoppingToken) in C:\Users\vijku\source\repos\CalendarApp\CalendarApp\Worker.cs:line 21
     at Microsoft.Extensions.Hosting.Internal.Host.TryExecuteBackgroundServiceAsync(BackgroundService backgroundService)
jzabroski commented 2 years ago

Please re-read the error message for next steps:

RazorLight.TemplateNotFoundException: RazorLightProjectItem of type RazorLight.Razor.EmbeddedRazorProjectItem with key Email.cshtml could not be found by the RazorLightProject of type RazorLight.Razor.EmbeddedRazorProject and does not exist in dynamic templates. See the "KnownDynamicTemplateKeys" and "KnownProjectTemplateKeys" properties for known template keys.

vijkumarsharma commented 2 years ago

@jzabroski yeah, I did get through an error, but no fruitful result as such. do you know how to fix it? I tried many things but no result.

saw this link as well https://github.com/toddams/RazorLight/issues/474

jzabroski commented 2 years ago

What is not fruitful?