Design and implement a template engine–part 2
In the Design and implement a template engine – part 1 I mentioned about a better solution: Reuse Razor Engine. In the post, I go in detail of how to implement it. But, first, we need to know some concepts:
Concepts
- Template Engine: Use to render a template into string
- Template Model Builder: Build the Model object which is used to fill in the data in template
- Template Model Expander: Expand the Model object with more properties
- And Template: the template
Implementation
The implementation assume that you already know about dynamic object and Razor Syntax (which is used in MVC – Views)
public interface ITemplateModelExpander { /// <summary> /// Expand a dynamic object with more properties. The input object is sharing between expanders. Each expander will expand the properties that most make sense to them /// </summary> /// <param name="obj"></param> /// <returns></returns> dynamic Expand(dynamic obj); /// <summary> /// True if want to tell the <see cref="ITemplateModelBuilder"/> that, use the result of Expand method as the input for the next expander. /// The implementation should be very careful when setting true. Most of the time, it should be false /// </summary> bool UseResultForNextExpander { get; } }
ITemplateModelExpander is the one that will be implemented in many classes. As its purpose, we want to expand the dynamic object to build up a real Model for the template.
Next we will see how we define a Template Model Builder.
/// <summary> /// Builder is for building the object model using the inner expanders /// </summary> public interface ITemplateModelBuilder { /// <summary> /// return the dynamic object that can be used in template /// </summary> /// <returns></returns> object Build(); /// <summary> /// Append expander /// </summary> /// <param name="expander"></param> void AppendExpander(ITemplateModelExpander expander); void AppendExpanders(IEnumerable<ITemplateModelExpander> expanders); /// <summary> /// Clear all expanders. It is useful when you want to empty the object model and start adding new expander. /// Mostly you will use it if there is a single <see cref="ITemplateModelBuilder"/> in the system (Singleton lifestyle) /// </summary> void ClearExpanders(); }
It does 1 main thing: Build; which builds the Model object (of course, dynamic object) using expanders. That’s why you see 3 more methods related to Expander.
Then I make a simple implementation for the Model Builder:
public class StandardTemplateModelBuilder : ITemplateModelBuilder { private readonly List<ITemplateModelExpander> _expanders = new List<ITemplateModelExpander>(); public object Build() { var model = new ExpandoObject(); foreach (var expander in _expanders) { var result = expander.Expand(model); if (expander.UseResultForNextExpander) { model = result; } } return model; } public void AppendExpander(ITemplateModelExpander expander) { _expanders.Add(expander); } public void AppendExpanders(IEnumerable<ITemplateModelExpander> expanders) { _expanders.AddRange(expanders); } public void ClearExpanders() { _expanders.Clear(); } }
Pretty simple right
Let’s talk about Template, I have this simple interface:
/// <summary> /// Define a template entry. /// </summary> public interface ITemplate { /// <summary> /// The template, which tell <see cref="ITemplateEngine"/> how to render output /// </summary> string Template { get; set; } /// <summary> /// The builder which will build the model to populate data for the template /// </summary> ITemplateModelBuilder ModelBuilder { get; } }
Define it as interface gives me power to change the template type. Even though, I just have Razor template for now:
public class RazorTemplate : ITemplate { public RazorTemplate(ITemplateModelBuilder modelBuilder) { if(modelBuilder == null) throw new ArgumentNullException("modelBuilder"); ModelBuilder = modelBuilder; } public string Template { get; set; } public ITemplateModelBuilder ModelBuilder { get; private set; } }
You might wander why do I not inject template property directly in the constructor? Because, I want to resolve the ITemplate instance using an IoC container, such as Windsor and wire up with correct ITemplateModelBuilder which is StandardTemplateModelBuilder in this case. (I will show you how to use it later)
The last part, the Template Engine:
/// <summary> /// Engine to render output base on template /// </summary> public interface ITemplateEngine { string Render(ITemplate template); }
Very simple. It just render an ITemplate instance.
The implementation is based on Razor Engine
/// <summary> /// Simple implementation using Razor engine /// </summary> public class RazorBasedTemplateEngine : ITemplateEngine { public string Render(ITemplate template) { return Razor.Parse(template.Template, template.ModelBuilder.Build()); } }
It asks the ModelBuilder from Template to build up the dynamic model object which is used to populate the template.
Usage
Have a look at this Unit Test:
[Test] public void Test_simple_render_engine() { // Arrange var engine = new RazorBasedTemplateEngine(); var modelBuilder = new StandardTemplateModelBuilder(); var template = new RazorTemplate(modelBuilder) { Template = @"Hello @Model.Name" }; modelBuilder.AppendExpander(new SampleByNameExpander("Thai Anh Duc")); // Act var result = engine.Render(template); // Assert Console.WriteLine(result); Assert.AreEqual("Hello Thai Anh Duc", result); }
I setup Engine, ModelBuilder, and Razor Template. These objects can be wire up automatically by IoC, see below
public class TestIoCWireUp { private readonly ITemplateEngine _engine; private readonly ITemplate _template; public TestIoCWireUp(ITemplateEngine engine, ITemplate template) { if (engine == null) throw new ArgumentNullException("engine"); if (template == null) throw new ArgumentNullException("template"); _engine = engine; _template = template; } public string RenderNameTemplate(string template) { _template.Template = template; _template.ModelBuilder.AppendExpander(new SampleByNameExpander("Thai Anh Duc")); return _engine.Render(_template); } }
And very simple implementation for SampleByNameExpander:
public class SampleByNameExpander : ITemplateModelExpander { private readonly string _name; public SampleByNameExpander(string name) { _name = name; } public dynamic Expand(dynamic obj) { obj.Name = _name; return obj; } public bool UseResultForNextExpander { get {return false;} } }
If you want to expand the Model with more properties that make sense to your application, you can add as many as possible. Just make sure you add the expander for the ModelBuilder of the Template you want. Or you can create a big giant Expander which will expose lot of properties at once. However, I will not encourage that. It is much more maintainable with simple small classes.
And, that’s it. Hope it help