DIY RazorEngine 的程序集生成方式

最近遇到一個項目,要使用RazorEngine作模板引擎,而後完成簡易的CMS功能,以減輕重複的CDRU操做,同時複用管理後臺。沒錯,使用的正是GIT HUB上的開源項目:https://github.com/Antaris/RazorEngine 。模板編譯過程很是耗時,因此Razor提供了Compile和Parse的帶key參數的重載,以實現從緩存中加載編譯後的模板的功能。不過這裏仍是有一個問題,對於web項目而言,應用程序池會週期性的回收(即便設置了不自動回收,不知爲什麼)。因此,仍然會存在從新編譯而致使頁面長時間掛起的問題。或許能夠提供一個進度條,告訴客戶這是一個高大上的東西,須要熱身....不過應該有更好的辦法,就是從RazorEngine自己着手。git

RazorEngine接收到模板內容的時候,會調用編輯器將其編譯成一個程序集,加載到內存中,同時返回編譯好的對應的模板的類型對象(Type對象)。而後調用對象的構造函數,生成對象實例,最後「執行」模板。BUT!爲何是加載到內存呢,若是是將程序集保存在磁盤上,那麼下次再進行讀取的話,這個性能絕對不是同一個檔次的。因此,考慮以後,決定用如下的思路解決問題:github

獲取到模板以後(字符串),會計算其MD5值,並做爲生成的模板類型的Class(有坑),同時將其做爲程序集的名稱。每當客戶機代碼請求編譯模板的時候,就去指定的目錄查找是否有這麼一個程序集,若是有,直接加載這個程序集,並返回類型信息(由於程序集中只有一個類型,因此很是方便)。RazorEngine自己採用了可擴展的設計。擴展口就在Razor.SetTemplateService()。只須要實現CompilerServiceBase抽象類,就能自定義模板引擎的邏輯。只是...代碼編譯部分處於調用的較底層,若是所有采用全新的實現的話...工做量很多,並且容易存在BUG。SO...原樣應用RazorEngine.dll並進行擴展這麼完美的事情暫時還辦不到。我採用的作法是,修改RazorEngine的源碼,可是不修改已經定義的類型,只在相應的名稱空間下面提供新的類型來知足本身的需求(由於其中很多須要的類型是internal的)。web

參考TemplateService的源碼,實現了一個ReloadableTemplateService,從新實現了CreateTemplateType方法:小程序

[Pure]
        public virtual Type CreateTemplateType(string razorTemplate, Type modelType)
        {
            //重要:類名不能以數字開頭
            var key = GetTemplateMd5(razorTemplate);
            string className = "C" + key;
            var assemblyPath = AssemblyDirecotry.TrimEnd(new[] { '/', '\\' }) + "\\" + key + ".dll";

            if (File.Exists(assemblyPath))
            {
                try
                {
                    //var assembly = Assembly.Load(File.ReadAllBytes(assemblyPath));
                    var assembly = Assembly.LoadFile(assemblyPath);
                    return assembly.GetTypes()[0];
                }
                catch (Exception ex)
                {
                    Debug.WriteLine("an error ocured and assembly has been deleted");
                    File.Delete(assemblyPath);
                }
            }

            var context = new TypeContext
            {
                ModelType = (modelType == null) ? typeof(object) : modelType,
                TemplateContent = razorTemplate,
                TemplateType = (_config.BaseTemplateType) ?? typeof(TemplateBase<>),
            };

            foreach (string ns in _config.Namespaces)
                context.Namespaces.Add(ns);

            //csharp only
            var service = new CSharpFileDirectCompilerService();

            service.Debug = _config.Debug;
            service.CodeInspectors = _config.CodeInspectors ?? Enumerable.Empty<ICodeInspector>();

            var result = service.CompileType(context, className, assemblyPath);
            _assemblies.Add(result.Item2);

            return result.Item1;
        }

須要注意是,C#的類型名稱不能以數字開頭...這個問題我查了一下午,主要是壓根沒有想到這一點。生成程序集的方法,被放置在CSharpFileDirectCompilerService中,因爲這個類型的侷限性很強(我只是想快速解決問題),因此沒有實現基類要求的方法(我把它廢了,雖然這樣很傻逼):緩存

[Pure]
        private Tuple<CompilerResults, string> Compile(TypeContext context, string className, string assemblyPath)
        {
            if (_disposed)
                throw new ObjectDisposedException(GetType().Name);

            var compileUnit = GetCodeCompileUnit(className, context.TemplateContent, context.Namespaces,
                context.TemplateType, context.ModelType);

            var @params = new CompilerParameters
            {
                GenerateInMemory = false,
                OutputAssembly = assemblyPath,
                GenerateExecutable = false,
                IncludeDebugInformation = false,
                CompilerOptions = "/target:library /optimize /define:RAZORENGINE"
            };

            var assemblies = CompilerServicesUtility
                .GetLoadedAssemblies()
                .Where(a => !a.IsDynamic && File.Exists(a.Location))
                .GroupBy(a => a.GetName().Name)
                .Select(grp => grp.First(y => y.GetName().Version == grp.Max(x => x.GetName().Version)))
                // only select distinct assemblies based on FullName to avoid loading duplicate assemblies
                .Select(a => a.Location);

            var includeAssemblies = (IncludeAssemblies() ?? Enumerable.Empty<string>());
            assemblies = assemblies.Concat(includeAssemblies)
                .Where(a => !string.IsNullOrWhiteSpace(a))
                .Distinct(StringComparer.InvariantCultureIgnoreCase);

            @params.ReferencedAssemblies.AddRange(assemblies.ToArray());

            string sourceCode = null;
            if (Debug)
            {
                var builder = new StringBuilder();
                using (var writer = new StringWriter(builder, CultureInfo.InvariantCulture))
                {
                    _codeDomProvider.GenerateCodeFromCompileUnit(compileUnit, writer, new CodeGeneratorOptions());
                    sourceCode = builder.ToString();
                }
            }

            return Tuple.Create(_codeDomProvider.CompileAssemblyFromDom(@params, compileUnit), sourceCode);
        }

        /// <summary>
        /// Compiles the type defined in the specified type context.
        /// </summary>
        /// <param name="context">The type context which defines the type to compile.</param>
        /// <returns>The compiled type.</returns>
        [Pure, SecurityCritical,Obsolete("該方法沒法兼容其父類,功能已經在其重載中提供")]
        public override Tuple<Type, Assembly> CompileType(TypeContext context)
        {
            throw new NotImplementedException("該方法沒法兼容其父類,功能已經在其重載中提供");
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        /// <param name="className"></param>
        /// <param name="assemblyDirectory"></param>
        /// <returns></returns>
        /// <exception cref="NullReferenceException"></exception>
        /// <exception cref="TemplateCompilationException"></exception>
        public Tuple<Type, Assembly> CompileType(TypeContext context, string className, string assemblyDirectory)
        {
            if (context == null)
                throw new NullReferenceException("context");
            var result = Compile(context, className, assemblyDirectory);
            var compileResult = result.Item1;

            if (compileResult.Errors != null && compileResult.Errors.HasErrors)
                throw new TemplateCompilationException(compileResult.Errors, result.Item2, context.TemplateContent);

            return Tuple.Create(
                compileResult.CompiledAssembly.GetType("CompiledRazorTemplates.Dynamic." + className),
                compileResult.CompiledAssembly);
        }

而後就成了,調用方式是:編輯器

            Razor.SetTemplateService(new ReloadableTemplateService()
            {
                AssemblyDirecotry = "d:\\temple"
            });

稍微測了下性能,這裏定性描述下:編譯模板的時候,耗時會根據模板的大小和複雜度,一兩秒或者更多。而加載一個程序集的話,尤爲是像這種一個類型的小程序集,老是毫秒級的。
真是個愉快的週末。ide

相關文章
相關標籤/搜索