Antaris / RazorEngine

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

Best practices for web app #2

Closed Madd0g closed 12 years ago

Madd0g commented 12 years ago

Hi,

I want to use RazorEngine in an MVC app. I want to have several templates that could be used in several places in the application. What are the best practices for using the library in a web app - should I just be using using (var service = new TemplateService())

wherever I want to use the templates? Will caching work if the template service is called like that? Or should I have a singleton somewhere?

Also, I've read that the previous version had support for pre-compiling the templates, I couldn't find the method Compile in Razor, so is it not in the new version yet? What's the difference between caching and pre-compiling anyway?

Thank you

Antaris commented 12 years ago

Hi,

Caching is scoped per instance of a template service, which means that if you are spinning up new instances each time then you'll lose your cached templates. I'm investigating whether to make cached templates global, might add in a configuration option to enable this.

As for recommendations, you'll probably simply want to use the Razor static type, which maintains a singleton instance of a template service for you. This singleton instance can be replaced with your own preconfigured instance of an ITemplateService. To minimise the number of recompilations for your web-app, this is the route I would take.

Pre-compilation is still supported, but the Compile method hasn't been implemented. Essentially making a call to GetTemplate will achieve the same thing, but I will be adding the Compile (and Run) method(s) back in for backwards compatibility.

Madd0g commented 12 years ago

Thanks for the reply.

Is there a way to use the ITemplate that is returned from GetTemplate and CreateTemplate to be filled with a model? Like if I want to keep a reference to the template I get from CreateTemplate and use it directly.

I see Execute and Run, but one doesn't accept parameters and the other accepts ExecuteContext and I haven't seen examples of how it can be used.

Antaris commented 12 years ago

Hi,

ExecuteContext is an implementation detail, you can easily spin up a new instance of it: new ExecuteContext(). Normally the framework itself will handle this, as it is used to track sections and layouts (stacking) while the template is executing. We can't easily return an instance of ITemplate<T> because we can't return a generic type when the model is anonymous or dynamic. This means we only return an instance of ITemplate.

If you know your model type, you can easily cast back to the base template type, e.g.:

var tmp = (TemplateBase<MyModel>)instance;
tmp.Model = model;

The Execute and Run methods have specific purposes. The Execute method is what is generated by the Razor parser, it is void which means we can't return anything, and it essentially just builds the merged content of the result and sets it into an internal writer.

The Run method simply executes the Execute method, and returns the string result.

When I get round to implementing Run on the ITemplateService, I'll take care of the ExecuteContext instantiation for you

Madd0g commented 12 years ago

var tmp = (TemplateBase<MyModel>)instance; tmp.Model = model;

Ah... very nice, this should definitely be in the documentation. If there was a helper to do this work - it would feel a lot more natural.

Antaris commented 12 years ago

LOL, as with the code, the documentation is work-in-progress ;-).

I think the Run method will essentially suit your needs. 9/10 times you'll only ever be using the template service operations (Parse, ParseMany, etc.), so interacting with the template instances themselves should be a pretty sporadic use case.

Watch this space for updates :-)

Antaris commented 12 years ago

The beta release that dropped last night (v3.0.0beta) includes support for the Compile and Run helper methods. These are defined in ITemplateService, and also made part of the Razor static type.

Let me know if you have any issues.

Madd0g commented 12 years ago

I just want to make sure, if I keep a reference to ITemplate - I don't have to worry about naming the template and such?

This is my usage, it's a bit weird, because of the double cast when parsing the template, but it works - but does it correctly cache?

Creation:

var type = Type.GetType(sModelType);
var typeInstance = (IMyInterface)Activator.CreateInstance(type);
template.CompiledTemplate = Service.CreateTemplate(sTemplate, typeInstance);

Usage:

var concreteTemplate = (TemplateBase<IMyInterface>) template.CompiledTemplate;
concreteTemplate.Model = model;    
var result = ((ITemplate) concreteTemplate).Run(new ExecuteContext());

If I keep a reference to template.CompiledTemplate and run it like this - will caching work?

Thank you!

Antaris commented 12 years ago

Well, the type itself will not be cached, but you'll have an instance of ITemplate that you hold onto anyway. It's important that you always use Run instead of Execute though, as Run will create our buffer that content will be written into, which then returns the result.

This might get a little more complicated when you want to use Layouts/Sections or Includes, because it will need to call back to the ITemplateService to resolve those templates.

Madd0g commented 12 years ago

Thanks, as long as it's going to be possible, I don't mind adding extra code to support Layouts (I will have a reference to the template service whenever I use the stored ITemplate).

Antaris commented 12 years ago

The other thing to watch out for, is that ITemplate by itself is not threadsafe. You can't use the Run method concurrently, as it only has a single buffer to write to. You'll end up with mangled content if you do...

Madd0g commented 12 years ago

that's not optimal - so my only option is to use the named templates?

that's a bit awkward, the choice is between creating and storing a guid for each and every thing I want to cache, I also thought of using glued names (model.id + model.categoryid) as a key - but I have a ienumerable member with it's own templates and it gets so much more complicated because they don't have ids. Not to mention that strings are horrible for maintenance.

I'm a bit disappointed, I can't see how to accomplish anything elegant with strings without writing a huge helper to abstract the strings away.

Antaris commented 12 years ago

I think what you need to do, is expose template activation instead. The ITemplateService supports the operation CreateTemplateType which returns the Type instance of your compiled type. You could store that, and perhaps if I make the protected method CreateTemplate<T>(Type type, T model) public, you could then call back to the TemplateService to activate the template, so essentially you create a new instance each time you want (thus bypassing any threading issue). Then perhaps if your code did something like:

public class ConcreteInstance
{
    public Type TemplateType { get; set; }
    pulibic TemplateService TemplateService { get; set; }

    public ITemplate CreateTemplate<T>(T model) 
    {
        return this.TemplateService.CreateTemplate(TemplateType, model);
    }
}

? I'd need to make an update to support that, but if that helps it can be done...