//****************************************************************************************************** // 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 } }