Skip to content

Instantly share code, notes, and snippets.

@benmccallum
Last active November 3, 2021 08:15
Show Gist options
  • Save benmccallum/ab64f647e1a590970ab17a02d63e938a to your computer and use it in GitHub Desktop.
Save benmccallum/ab64f647e1a590970ab17a02d63e938a to your computer and use it in GitHub Desktop.
RazorEngineCore HTML encoding with layout

Based on the docs at https://github.com/adoconnection/RazorEngineCore/wiki, but modified to support a layout in conjuction with HTML encoding.

Specifically, the modifications avoid a double-encoding of the HTML of the body/includes when added to the layout, by overriding in the HtmlTemplateBase the RenderBody and Include methods to wrap the content as RawContent.

Modifications from wiki docs:

  1. Modify MyTemplateBase Include and RenderBody methods to return object and be virtual.
  2. Modify MyHtmlTemplateBase (which inherits MyTemplateBase) by overriding those two methods and have them return Raw(base.Include/RenderBody);.
  3. Need to move RawContent into its own internal class as it was previously private inside MyTemplateBase.

Like this, when WriteAsync is called with the RenderBody() or Include() result, it's already a RawContent and the avoidance of html encoding happens.

using System;
using System.Collections.Generic;
using System.Linq;
using RazorEngineCore;
namespace MyCompany.Templating.RazorEngineCore
{
public static class IRazorEngineExtensions
{
// See:
// https://github.com/adoconnection/RazorEngineCore/wiki/@Include-and-@Layout
public static MyCompiledTemplate<MyHtmlTemplateBase<TModel>, TModel> CompileHtml<TModel>(
this IRazorEngine razorEngine,
string template,
IDictionary<string, string>? includes)
{
return new MyCompiledTemplate<MyHtmlTemplateBase<TModel>, TModel>(
razorEngine.Compile<MyHtmlTemplateBase<TModel>>(template, ConfigureOpts),
includes?.ToDictionary(
k => k.Key,
v => razorEngine.Compile<MyHtmlTemplateBase<TModel>>(v.Value, ConfigureOpts))
?? new Dictionary<string, IRazorEngineCompiledTemplate<MyHtmlTemplateBase<TModel>>>(0));
}
public static MyCompiledTemplate<MyTemplateBase<TModel>, TModel> CompileNonHtml<TModel>(
this IRazorEngine razorEngine,
string template,
IDictionary<string, string>? includes)
{
return new MyCompiledTemplate<MyTemplateBase<TModel>, TModel>(
razorEngine.Compile<MyTemplateBase<TModel>>(template, ConfigureOpts),
includes?.ToDictionary(
k => k.Key,
v => razorEngine.Compile<MyTemplateBase<TModel>>(v.Value, ConfigureOpts))
?? new Dictionary<string, IRazorEngineCompiledTemplate<MyTemplateBase<TModel>>>(0));
}
private static void ConfigureOpts(IRazorEngineCompilationOptionsBuilder opts)
{
opts.AddAssemblyReference(typeof(Math));
opts.AddAssemblyReferenceByName("System.Collections");
opts.AddUsing("System");
opts.AddUsing("System.Collections");
//opts.AddAssemblyReferenceByName("MyCompany.Xyz");
}
}
}
using System.Collections.Generic;
using RazorEngineCore;
namespace MyCompany.Templating.RazorEngineCore
{
public class MyCompiledTemplate<TTemplate, TModel>
where TTemplate : MyTemplateBase<TModel>
{
private readonly IRazorEngineCompiledTemplate<TTemplate> _compiledTemplate;
private readonly Dictionary<string, IRazorEngineCompiledTemplate<TTemplate>> _compiledParts;
public MyCompiledTemplate(
IRazorEngineCompiledTemplate<TTemplate> compiledTemplate,
Dictionary<string, IRazorEngineCompiledTemplate<TTemplate>> compiledParts)
{
_compiledTemplate = compiledTemplate;
_compiledParts = compiledParts;
}
public string Run(TModel model)
{
return Run(_compiledTemplate, model);
}
public string Run(IRazorEngineCompiledTemplate<TTemplate> template, TModel model)
{
TTemplate? templateReference = null;
var result = template.Run(instance =>
{
instance.Model = model;
instance.IncludeCallback = (key, includeModel) => Run(_compiledParts[key], includeModel);
templateReference = instance;
});
if (templateReference?.Layout == null)
{
return result;
}
return _compiledParts[templateReference.Layout].Run(instance =>
{
instance.Model = model;
instance.IncludeCallback = (key, includeModel) => Run(_compiledParts[key], includeModel);
instance.RenderBodyCallback = () => result;
});
}
}
}
using System.Net;
using System.Threading.Tasks;
namespace MyCompany.Templating.RazorEngineCore
{
/// <summary>
/// A template base that should be used for generating HTML
/// as it encodes model data writing by default.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <remarks>
/// https://github.com/adoconnection/RazorEngineCore/wiki/@Raw
/// </remarks>
public class MyHtmlTemplateBase<T> : MyTemplateBase<T>
{
public object Raw(object value)
{
return new RawContent(value);
}
public override object Include(string key, T model)
{
return Raw(base.Include(key, model));
}
public override object RenderBody()
{
return Raw(base.RenderBody());
}
public override Task WriteAsync(object? obj = null)
{
var value = obj is RawContent rawContent
? rawContent.Value
: WebUtility.HtmlEncode(obj?.ToString());
return base.WriteAsync(value);
}
public override Task WriteAttributeValueAsync(
string prefix, int prefixOffset, object? value,
int valueOffset, int valueLength, bool isLiteral)
{
value = value is RawContent rawContent
? rawContent.Value
: WebUtility.HtmlEncode(value?.ToString());
return base.WriteAttributeValueAsync(
prefix, prefixOffset, value, valueOffset, valueLength, isLiteral);
}
}
}
using System;
using RazorEngineCore;
namespace MyCompany.Templating.RazorEngineCore
{
/// <summary>
/// A template base that supports layout and include concepts.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <remarks>
/// https://github.com/adoconnection/RazorEngineCore/wiki/@Include-and-@Layout
/// </remarks>
public class MyTemplateBase<T> : RazorEngineTemplateBase<T>
{
public Func<string, T, object> IncludeCallback { get; set; } = null!;
public Func<object> RenderBodyCallback { get; set; } = null!;
public string? Layout { get; set; }
public virtual object Include(string key, T model)
{
return IncludeCallback(key, model);
}
public virtual object RenderBody()
{
return RenderBodyCallback();
}
}
}
namespace MyCompany.Templating.RazorEngineCore
{
/// <summary>
/// Wrapping type to identify already-encoded, safe HTML
/// that can be rendered as is into a template.
/// </summary>
internal class RawContent
{
public object Value { get; set; }
public RawContent(object value)
{
Value = value;
}
}
}
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using RazorEngineCore;
namespace MyCompany.Templating.RazorEngineCore
{
public class RazorCoreTemplateEngine : ITemplateEngine
{
private readonly IRazorEngine _razorEngine;
private readonly ConcurrentDictionary<string, object> _htmlCache = new();
private readonly ConcurrentDictionary<string, object> _nonHtmlCache = new();
public RazorCoreTemplateEngine(
IRazorEngine razorEngine)
{
_razorEngine = razorEngine;
}
public ValueTask<string> RunForHtmlAsync<TViewModel>(
string viewTemplateName,
string viewTemplate,
TViewModel viewModel,
string? layoutTemplateName = null,
string? layoutTemplate = null,
IDictionary<string, string>? includes = null)
{
var compiledTemplate = GetCompiledHtmlTemplate<TViewModel>(
viewTemplateName,
viewTemplate,
layoutTemplateName,
layoutTemplate,
includes);
var result = compiledTemplate.Run(viewModel);
return new ValueTask<string>(result);
}
public MyCompiledTemplate<MyHtmlTemplateBase<TViewModel>, TViewModel> GetCompiledHtmlTemplate<TViewModel>(
string viewTemplateName,
string viewTemplate,
string? layoutTemplateName = null,
string? layoutTemplate = null,
IDictionary<string, string>? includes = null)
{
var cacheKey = GetCacheKey(viewTemplateName, layoutTemplateName, includes);
return (MyCompiledTemplate<MyHtmlTemplateBase<TViewModel>, TViewModel>)
_htmlCache.GetOrAdd(cacheKey, key =>
{
includes = AddTemplateToIncludes(layoutTemplateName, layoutTemplate, includes);
return _razorEngine.CompileHtml<TViewModel>(viewTemplate, includes);
});
}
public ValueTask<string> RunForNonHtmlAsync<TViewModel>(
string viewTemplateName,
string viewTemplate,
TViewModel viewModel,
string? layoutTemplateName = null,
string? layoutTemplate = null,
IDictionary<string, string>? includes = null)
{
var compiledTemplate = GetCompiledTemplate<TViewModel>(
viewTemplateName,
viewTemplate,
layoutTemplateName,
layoutTemplate,
includes);
var result = compiledTemplate.Run(viewModel);
return new ValueTask<string>(result);
}
public MyCompiledTemplate<MyTemplateBase<TViewModel>, TViewModel> GetCompiledTemplate<TViewModel>(
string viewTemplateName,
string viewTemplate,
string? layoutTemplateName = null,
string? layoutTemplate = null,
IDictionary<string, string>? includes = null)
{
var cacheKey = GetCacheKey(viewTemplateName, layoutTemplateName, includes);
return (MyCompiledTemplate<MyTemplateBase<TViewModel>, TViewModel>)
_nonHtmlCache.GetOrAdd(cacheKey, key =>
{
includes = AddTemplateToIncludes(layoutTemplateName, layoutTemplate, includes);
return _razorEngine.CompileHtml<TViewModel>(viewTemplate, includes);
});
}
private static string GetCacheKey(
string viewTemplateName,
string? layoutTemplateName = null,
IDictionary<string, string>? includes = null)
{
return $"{layoutTemplateName ?? "NoLayout"}_" +
$"{viewTemplateName}_" +
$"{(includes == null ? "NoIncludes" : string.Join(",", includes.Select(i => i.Key)))}";
}
private static IDictionary<string, string>? AddTemplateToIncludes(
string? layoutTemplateName,
string? layoutTemplate,
IDictionary<string, string>? includes)
{
// The engine requires the layout in the includes, but for convenience we make that easy for callers...
// But now we need to include it
if (layoutTemplate is not null)
{
if (layoutTemplateName is null)
{
throw new ArgumentNullException(
nameof(layoutTemplateName),
$"If a {nameof(layoutTemplate)} is provided, a {nameof(layoutTemplateName)} should be too.");
}
includes ??= new Dictionary<string, string>(1);
includes.Add(layoutTemplateName, layoutTemplate);
}
return includes;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment