//******************************************************************************************************
// RazorEngine.cs - Gbtc
//
// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
// file except in compliance with the License. You may obtain a copy of the License at:
//
// http://opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 05/05/2016 - J. Ritchie Carroll
// Generated original version of source code.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Web.Hosting;
using GSF.Collections;
using GSF.Configuration;
using GSF.IO;
using RazorEngine;
using RazorEngine.Configuration;
using RazorEngine.Templating;
namespace GSF.Web.Model
{
///
/// Defines a Razor engine instance for the specified constraint.
///
/// for the Razor engine.
public class RazorEngine : IRazorEngine where TLanguage : LanguageConstraint, new()
{
#region [ Members ]
// Nested Types
private sealed class FileWatchDebugTemplateManager : ITemplateManager, IDisposable
{
#region [ Members ]
// Fields
private readonly ResolvePathTemplateManager m_templateManager;
private readonly InvalidatingCachingProvider m_cachingProvider;
private readonly SafeFileWatcher m_fileWatcher;
private bool m_disposed;
#endregion
#region [ Constructors ]
public FileWatchDebugTemplateManager(string layoutRoot, InvalidatingCachingProvider cachingProvider)
{
m_cachingProvider = cachingProvider;
m_templateManager = new ResolvePathTemplateManager(new[] { layoutRoot });
m_fileWatcher = new SafeFileWatcher(Path.GetFullPath(layoutRoot), "*.*")
{
EnableRaisingEvents = true,
IncludeSubdirectories = true
};
m_fileWatcher.Changed += m_fileWatcher_Changed;
m_fileWatcher.Created += m_fileWatcher_Changed;
m_fileWatcher.Deleted += m_fileWatcher_Changed;
m_fileWatcher.Renamed += m_fileWatcher_Changed;
}
#endregion
#region [ Methods ]
public void Dispose()
{
if (m_disposed)
return;
try
{
if ((object)m_fileWatcher != null)
{
m_fileWatcher.Changed -= m_fileWatcher_Changed;
m_fileWatcher.Created -= m_fileWatcher_Changed;
m_fileWatcher.Deleted -= m_fileWatcher_Changed;
m_fileWatcher.Renamed -= m_fileWatcher_Changed;
m_fileWatcher.Dispose();
}
}
finally
{
m_disposed = true;
}
}
public ITemplateSource Resolve(ITemplateKey key)
{
return m_templateManager.Resolve(key);
}
public ITemplateKey GetKey(string name, ResolveType resolveType, ITemplateKey context)
{
return m_templateManager.GetKey(name, resolveType, context);
}
public void AddDynamic(ITemplateKey key, ITemplateSource source)
{
m_templateManager.AddDynamic(key, source);
}
private void m_fileWatcher_Changed(object sender, FileSystemEventArgs e)
{
m_cachingProvider.InvalidateAll();
}
#endregion
}
// Fields
private readonly IRazorEngineService m_engineService;
private bool m_disposed;
#endregion
#region [ Constructors ]
///
/// Creates a new instance.
///
/// Template path for view files.
public RazorEngine(string templatePath)
{
if (string.IsNullOrEmpty(templatePath))
throw new ArgumentNullException(nameof(templatePath));
TLanguage languageType = new TLanguage();
switch (languageType.ResolutionMode)
{
case RazorViewResolutionMode.ResolvePath:
case RazorViewResolutionMode.WatchingResolvePath:
if (!Directory.Exists(templatePath))
throw new DirectoryNotFoundException();
if (languageType.ResolutionMode == RazorViewResolutionMode.ResolvePath)
{
m_engineService = RazorEngineService.Create(new TemplateServiceConfiguration
{
Language = languageType.TargetLanguage,
TemplateManager = new ResolvePathTemplateManager(new[] { templatePath }),
Debug = false
});
}
else
{
// The watching resolve path template manager should not be used in production since
// assemblies cannot be unloaded from an AppDomain. Every time a change to a .cshtml
// file is picked up by the watcher it is compiled and loaded into the AppDomain and
// the old one cannot be removed (.NET restriction), the net result is a memory leak
InvalidatingCachingProvider cachingProvider = new InvalidatingCachingProvider();
m_engineService = RazorEngineService.Create(new TemplateServiceConfiguration
{
Language = languageType.TargetLanguage,
CachingProvider = cachingProvider,
TemplateManager = new FileWatchDebugTemplateManager(templatePath, cachingProvider),
Debug = true
});
}
break;
case RazorViewResolutionMode.EmbeddedResource:
m_engineService = RazorEngineService.Create(new TemplateServiceConfiguration
{
Language = languageType.TargetLanguage,
TemplateManager = new DelegateTemplateManager(name =>
{
string resourceName = name;
if (!resourceName.Contains("."))
resourceName = templatePath + name;
Stream stream = WebExtensions.OpenEmbeddedResourceStream(resourceName);
if ((object)stream == null)
return "";
using (StreamReader reader = new StreamReader(stream))
return reader.ReadToEnd();
}),
Debug = false
});
break;
}
TemplatePath = templatePath;
}
#endregion
#region [ Properties ]
///
/// Gets the template path defined for this .
///
public string TemplatePath
{
get;
}
///
/// Gets the instance used by the .
///
public IRazorEngineService EngineService => m_engineService;
#endregion
#region [ Methods ]
///
/// Releases all the resources used by the object.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool disposing)
{
if (!m_disposed)
{
try
{
if (disposing)
m_engineService?.Dispose();
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
}
}
}
///
/// Gets a given key from the implementation. See .
///
///
public ITemplateKey GetKey(string name, ResolveType resolveType = ResolveType.Global, ITemplateKey context = null)
{
return m_engineService.GetKey(name, resolveType, context);
}
/// Checks if a given template is already cached.
///
public bool IsTemplateCached(ITemplateKey key, Type modelType)
{
return m_engineService.IsTemplateCached(key, modelType);
}
///
/// Adds a given template to the template manager as dynamic template.
///
public void AddTemplate(ITemplateKey key, ITemplateSource templateSource)
{
m_engineService.AddTemplate(key, templateSource);
}
///
/// Compiles the specified template and caches it.
///
public void Compile(ITemplateKey key, Type modelType = null)
{
m_engineService.Compile(key, modelType);
}
///
/// Compiles the specified template, by name, and caches it.
///
public void Compile(string name, Type modelType = null)
{
m_engineService.Compile(name, modelType);
}
///
/// Runs the given cached template. When the cache does not contain the template it will be compiled and cached beforehand.
///
public void RunCompile(ITemplateKey key, TextWriter writer, Type modelType = null, object model = null, DynamicViewBag viewBag = null)
{
m_engineService.RunCompile(key, writer, modelType, model, viewBag);
}
///
/// Runs the given cached template.
///
public void Run(ITemplateKey key, TextWriter writer, Type modelType = null, object model = null, DynamicViewBag viewBag = null)
{
m_engineService.Run(key, writer, modelType, model, viewBag);
}
///
/// Kicks off a task to pre-compile Razor templates.
///
/// Exception handler used to report issues, if any.
public Task PreCompile(Action exceptionHandler)
{
return PreCompile(exceptionHandler, null);
}
///
/// Kicks off a task to pre-compile Razor templates.
///
/// Exception handler used to report issues, if any.
/// Template path is use; otherwise, set to null to use default path.
public Task PreCompile(Action exceptionHandler, string templatePath)
{
return PreCompile(null, exceptionHandler, templatePath);
}
///
/// Kicks off a task to pre-compile Razor templates.
///
/// The type of the model used for the application.
/// Exception handler used to report issues, if any.
/// Template path is use; otherwise, set to null to use default path.
public Task PreCompile(Type modelType, Action exceptionHandler, string templatePath)
{
if (string.IsNullOrEmpty(templatePath))
templatePath = TemplatePath;
return Task.Run(() =>
{
TLanguage languageType = new TLanguage();
if (languageType.ResolutionMode == RazorViewResolutionMode.EmbeddedResource)
{
foreach (string fileName in Assembly.GetExecutingAssembly().GetManifestResourceNames().Where(fileName => fileName.StartsWith(templatePath)))
{
try
{
m_engineService.Compile(fileName, modelType);
}
catch (Exception ex)
{
if ((object)exceptionHandler != null)
exceptionHandler(new InvalidOperationException($"Failed to pre-compile razor template \"{fileName}\": {ex.Message}", ex));
}
}
}
else
{
string webRootFolder = FilePath.AddPathSuffix(templatePath);
string[] razorFiles = FilePath.GetFileList($"{webRootFolder}*.{(languageType.TargetLanguage == Language.CSharp ? "cs" : "vb")}html");
foreach (string fileName in razorFiles)
{
try
{
m_engineService.Compile(FilePath.GetFileName(fileName), modelType);
}
catch (Exception ex)
{
if ((object)exceptionHandler != null)
exceptionHandler(new InvalidOperationException($"Failed to pre-compile razor template \"{fileName}\": {ex.Message}", ex));
}
}
}
});
}
#endregion
#region [ Static ]
// Static Fields
private static RazorEngine s_defaultEngine;
private static readonly Dictionary> s_configuredEngines;
// Static Properties
///
/// Gets default configured razor engine instance.
///
public static RazorEngine Default => s_defaultEngine ?? (s_defaultEngine = GetConfiguredEngine());
// Static Constructor
static RazorEngine()
{
s_configuredEngines = new Dictionary>(StringComparer.OrdinalIgnoreCase);
}
// Static Methods
///
/// Gets a shared instance created based on configured template path defined in the specified .
///
/// Settings category to use for template path settings; defaults to "systemSettings".
/// Shared instance created based on configured template path.
[SuppressMessage("Microsoft.Reliability", "CA2002:DoNotLockOnObjectsWithWeakIdentity")]
public static RazorEngine GetConfiguredEngine(string settingsCategory = null)
{
lock (typeof(LanguageConstraint))
{
if (string.IsNullOrWhiteSpace(settingsCategory))
settingsCategory = "systemSettings";
return s_configuredEngines.GetOrAdd(settingsCategory, category =>
{
string templatePath = null;
// Get configured template path
CategorizedSettingsElementCollection settings = ConfigurationFile.Current.Settings[category];
TLanguage languageType = new TLanguage();
switch (languageType.ResolutionMode)
{
case RazorViewResolutionMode.ResolvePath:
case RazorViewResolutionMode.WatchingResolvePath:
if (Common.GetApplicationType() == ApplicationType.Web)
{
settings.Add("TemplatePath", "~/Views/Shared", "Path for data context based razor field templates.");
templatePath = HostingEnvironment.MapPath(settings["TemplatePath"].Value).EnsureEnd('/');
}
else
{
settings.Add("TemplatePath", "Eval(systemSettings.WebRootPath)", "Path for data context based razor field templates.");
templatePath = FilePath.GetAbsolutePath(settings["TemplatePath"].Value);
}
break;
case RazorViewResolutionMode.EmbeddedResource:
settings.Add("EmbeddedTemplatePath", "GSF.Web.Model.Views.", "Embedded name space path for data context based razor field templates.");
templatePath = settings["EmbeddedTemplatePath"].Value.EnsureEnd('.');
break;
}
return new RazorEngine(templatePath);
});
}
}
#endregion
}
}