Antaris / RazorEngine

Open source templating engine based on Microsoft's Razor parsing engine
http://antaris.github.io/RazorEngine
Other
2.13k stars 576 forks source link

MVC-Like TemplateManager and advanced XML based configuration. #271

Open litera opened 9 years ago

litera commented 9 years ago

I'm using RazorEngine to generate emails. To simplify general email visual aspect I'm trying to use a layout page but when I try to generate my emails I'm getting this error:

Please either set a template manager to resolve templates or add the template '~/Views/Shared/_LayoutEmail.cshtml'!

I'm not sure why I'm getting this error, because my layout page exists at the specified location. It is true that layout is parent folder of my actual files that are used for the email.

Folder structure

Views
  `-- Shared
        |-- Emails
        |     |-- Email1.cshtml
        |     `-- Email2.cshtml
        |
        `-- _LayoutEmail.cshtml

As can be seen my layout is in the parent folder of my actual email razor files if that makes any difference.

Layout (simplified)

<!DOCTYPE HTML>
<html>
    <body>
        @RenderBody()
    </body>
</html>

Email (simplified)

@model string
@{
    Layout = "~/Views/Shared/_LayoutEmail.cshtml";
}
<h1>Hello @Model</h1>
matthid commented 9 years ago

RazorEngine is not a complete ASP.Net MVC replacement. If you want to resolve templates you need to tell RazorEngine how you want it to resolve templates by implementing ITemplateManager and setting it in the configuration. If you happen to write a MVC compatible ITemplateManager I would be happy to accept a contribution (pull request) to integrate it into RazorEngine :)

That said you probable just want to do the following:

Engine.Razor.Compile("--layout content--", "layout" /*see below*/ )
// ... later ...
Engine.Razor.RunCompile("-- email1 template content --", "email1", model)

and your template:

@model string
@ { Layout = "layout"; /*same key as above*/ }
<h1>Hello @Model</h1>
litera commented 9 years ago

Is there a RazorEngine-specific configuration where I could set like:

<razorEngine>
    <templates>
        <add name="layout" location="~/views/shared/_layoutEmail.cshtml" />
    </templates>
</razorEngine>

and simplify things considerably?

matthid commented 9 years ago

There is xml based configuration, but I'm not aware of specifying single templates. But you could get away with something like this:

<configuration>
    <configSections>
        <section name="razorEngine" type="RazorEngine.Configuration.RazorEngineConfigurationSection, RazorEngine" requirePermission="false" />
    </configSections>
</configuration>
<razorEngine templateManagerType="My.TemplateManager" >
</razorEngine>
<myTemplates>
        <add name="layout" location="~/views/shared/_layoutEmail.cshtml" />
</myTemplates>

But again you need to write the My.TemplateManager yourself to do the heavy lifting and resolving the templates. Setting the templateMangerType type is supported already. So if your goal is to not change your current code this is entirely possible.

litera commented 9 years ago

Ok. So now I've implemented additional configuration element collection to also configure layouts that I precompile before I generate my emails.

<myconfig>
    <layouts>
        <add path="~/Views/Shared/_LayoutEmail.cshtml" />
    </layouts>
</myconfig>

Initially I enumerate through these layouts (currently there's only one at the moment) and execute the following

if (!Engine.Razor.IsTemplateCached(layout.Path, null))
{
    Engine.Razor.Compile(
        File.ReadAllText(HostingEnvironment.MapPath(layout.Path)),
        layout.Path);
}

This works and it caches precompiled layouts. As you can see I'm using file location as template key so my actual emails can later set their Layout to the actual layout file location (because keys match) and it should work.

But it doesn't. I'm now getting a different error while I call Engine.Razor.RunCompile() for my actual email that defines a layout:

The same key was already used for another template!

What else am I doing wrong? Mind that this used to work until I introduces layouts. So the actual email generation used to work up until now. I can also see that my layout template is being cached.

I haven't changed anything about template manager though. All I did was precompile and cache my layout, so when engine later parses my email it will see the preset Layout and it should be able to get it from cache and then create my email result. Is there anything else I should be doing as well?

matthid commented 9 years ago

Can you post a complete standalone repro so I can look into it? This exception usually indicates that you try to use two different templates with the same template key, see here. This can happen if you edit your template files and force a re-compile. Or (mistakenly) use the same key for two different templates.

matthid commented 9 years ago

This is psychic debugging but maybe you use something like:

Engine.Razor.Compile(
        File.ReadAllText(HostingEnvironment.MapPath("Email1.cshtml")),
        "Email");
// And then later
Engine.Razor.Compile(
        File.ReadAllText(HostingEnvironment.MapPath("Email2.cshtml")), // <- different template
        "Email"); // <- same key
litera commented 9 years ago

No my template keys are different. Layout's key equals its virtual location (as explained previously), and then I have two additional email keys that differ by type. In my erroneous case their names are Request.txt and Request.html. No key name clashes. Text template doesn't use any layout as it's just text, but HTML version does use previously compiled layout.

I'm parsing my emails using this line of code:

mail.AlternateViews.Add(
    AlternateView.CreateAlternateViewFromString(
        Engine.Razor.RunCompile(part.GetTemplate(type), part.ToString(type), typeof(T), model),
        part.MimeType));
matthid commented 9 years ago

Without a minimal repro I can only guess. And I have already guessed before :). So please provide a standalone repro, because I cannot know what your code does.

litera commented 9 years ago

I don't know if this is a solution (better yet a workaround) but things started working when I added a model to my layout and set it to dynamic. If this is a workaround (for the time being) it should be documented as I expect many people would love to use layouts with RazorEngine.

So adding to the top of layout page

@model dynamic

does the trick or at least it seems that way.

And by a repro would you want me to create a console app that reproduces the issue? That would require quite a bit of work I suppose. But is doable...

matthid commented 9 years ago

Hm now I think you probably hit some kind of bug. Normally the caching layer takes the model-type into consideration (and recompiled the template if required).

Yes a console application would be perfect, however anything how I can reproduce this on my machine would be acceptable. If your code is non-public you can maybe send the relevant parts via email (see my github profile)?

litera commented 9 years ago

I've tried dynamic model after reading this Stackoverflow answer and it actually did the trick. The problem is that my actual views use various different models but use the same layout.

matthid commented 9 years ago

If you are not using an outdated version this should have been fixed, I will try to add those tests... I have no idea what's going on in your scenario though, because you get another exception than the one from the post, right?

litera commented 9 years ago

Actually while I was trying to devise a console app to repro the issue it seemed I can't make it repro. Things worked. So I compared versions and I was indeed using a bit older version of RazorEngine in my Web app. 3.6.1. You should know whether this version is too old and layouts didn't work in it as they should.

But let's point out some other issue I encountered in the 3.6.1 version as well. When I mitigated my problem with layout (error: duplicate template key) I commented out layout definition from my templates like per usual razor commenting:

@*{
    Layout = "LayoutEmail.cshtml";
}*@

But razor engine apparently still wanted to set a layout to my template so I had to rename Layout to something else to make it ignore this.

Outcome

I've tried to devise a working example using 3.6.5, and then 3.6.1 but I couldn't. Everything worked as expected. There was difference related to reporting about manual temp files removal, but everything else worked as expected. I don't know hot to prepare an example about it...

I'm really confused about my app not working properly. Is there any way I can troubleshoot this so I could get to the bottom of the problem?

Question

I do have a question. I used to use generic methods for Compile and RunCompile. But I can also see that I can do layout compilation with non-extension ones. Like:

ITemplateKey key = Engine.Razor.GetKey(s, ResolveType.Layout);
if (!Engine.Razor.IsTemplateCached(key, null))
{
    LoadedTemplateSource source = new LoadedTemplateSource(File.ReadAllText(s), s);
    Engine.Razor.AddTemplate(key, source);
    Engine.Razor.Compile(key, null);
}

The question I'm having is whether setting ResolveType.Layout changes anything later while templates are being processed? Because if I use extension method

Engine.Razor.Compile(string, string);

it will add a template of type Global.

matthid commented 9 years ago

https://github.com/Antaris/RazorEngine/blob/master/doc/ReleaseNotes.md I don't see any relevant change.

There was difference related to reporting about manual temp files removal, but everything else worked as expected.

Yes more info (how to get rid of the warning): https://github.com/Antaris/RazorEngine/issues/244

Is there any way I can troubleshoot this so I could get to the bottom of the problem?

Maybe the strack-trace of the exception would help.

The question I'm having is whether setting ResolveType.Layout changes anything later while templates are being processed?

No it doesn't, this is a hint to the ITemplateResolver of what is being resolved. This is completely ignored by the default implementation.

The sole reason I have added it is to be able to implement the structure you showed in your initial post (or even an extra layout folder). However I didn't actually implement it.

litera commented 9 years ago

...and the good thing is that upgrading my app to use 3.6.5 works without problems. I was able to remove @model dynamic from my layout file and I'm not getting any errors whatsoever.

matthid commented 9 years ago

Glad to hear! I re-added feature request and up-for-grabs for your initial requests.

JeroenvdBurg commented 7 years ago

To support MVC @include and layout in your Mailtemplates. I have created the following template configuration. This one resolves the template by name. Make sure you put the templates you include in the MailTemplates directory.

string virtualTemplatePath = "~/App_Data/MailTemplates/";
var _basePath = HttpContext.Current.Server.MapPath(virtualTemplatePath);

var config = new TemplateServiceConfiguration
  {
        TemplateManager = new DelegateTemplateManager(name =>
                {
                    string path = Path.Combine(_basePath, name);
                    return File.ReadAllText(path);
                }
    )};           
_service = RazorEngineService.Create(config);
Engine.Razor = _service;

Maybe this helps, at least it works for me. (p.s I use the static version of RazorEngine, however this is not necesary)

wwwebconcepts commented 7 years ago

I have a better email templating solution: RazorSmartMailer It's easier to use and more robust. RazorSmartMailer has advanced HTML email templating and email messaging. RazorSmartMailer supports Helpers.WebMail, System.NET.Mail, attachment uploads and embed, embed linked resources, and image processing including resize, crop, watermark, and add text. RazorSmartMailer also supports inline CSS via the PreMailer.Net assembly. Razor Smart Mailer is your complete email solution.

To use it with C# it will have to be compiled. There's full documentation in the repository.