更新:2023/7/11 建立: 2023/2/20
(github) https://github.com/mirrortom/razorservice
RazorEngineCore https://github.com/adoconnection/RazorEngineCore
参考项目RazorEngineCore,是一个.netcore版本的实现.基于新版本.NET 6.0,使用的基础库是:
razor是asp.net提供的一项功能,可以在html中写入c#程序,成为一个扩展名是cshtml或者razor的文件,在浏览时,服务端解析razor文件并发回最终的html文档.
在asp.net项目以外,例如控制台程序中,也可以使用这种功能,使用第3方实现的库.比较早出现的,功能也比较全面的库是 https://github.com/Antaris/RazorEngine ,在.net framework时代就已经有了.Asp.net的Razor基本功能,这个库都实现了.
通过学习RazorEngineCore项目,了解到Razor功能的实现过程,可以分为4个阶段:
// Step1,将razor源代码包装为RazorSourceDocument对象. // 使用RazorSourceDocument.Create(content,fileName)静态方法 // content是razor源代码,文件名字可以随机 RazorSourceDocument document = RazorSourceDocument.Create(content, Path.GetRandomFileName()); // Step2,建立RazorProjectEngine对象 RazorProjectEngine engine = RazorProjectEngine.Create ( RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action<RazorProjectEngineBuilder> configure ) // Step3,调用Engine对象的Process()方法得到 RazorCodeDocument 对象 // 第一个参数是RazorSourceDocument对象 RazorCodeDocument codeDocument= engine.Process ( RazorSourceDocument, null, new List<RazorSourceDocument>(), new List<TagHelperDescriptor>() ) // Step4,调用RazorCodeDocument的GetCSharpDocument()方法,得到cs源码对象 RazorCSharpDocument razorCSharpDocument = codeDocument.GetCSharpDocument(); // 最后,获取GeneratedCode属性得到cs源码文本 // razorCSharpDocument.GeneratedCode
到这里,Razor语言从源码到CS源码的处理部分完成.
// Step1,分析cs源码,得到语法树对象CSharpSyntaxTree SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(CsSource); // Step2,建立编译对象CSharpCompilation // assemblyName: 程序集名字,最后编译出来的程序集名字 // SyntaxTree: 被编译的cs语法树对象 // MetadataReference: 编译时需要引用的程序集 CSharpCompilation compilation = CSharpCompilation.Create ( string? assemblyName, IEnumerable<SyntaxTree>? syntaxTrees = null, IEnumerable<MetadataReference>? references = null, CSharpCompilationOptions? options = null ) // 第3步,调用Emit()进行编译 // EmitResult对象包含编译结果信息.其中Success属性指示编译是否成功,Diagnostics属性包含编译过程中产生的信息 // 如果编译成功,那么内存流中就有程序集文件字节数组 MemoryStream memoryStream = new(); EmitResult emitResult = compilation.Emit(memoryStream);
得到程序集后,用反射获取类型和方法执行.
var assembly = Assembly.Load(assemblyBytes); var type = assembly.GetType("RazorService.Template"); var instance = Activator.CreateInstance(type) as TemplateBase; instance.ExecuteAsync();
程序集里的类和方法是Template.ExecuteAsync() 这和生成的CS代码相关.
Razor源代码生成为CS代码示例如下
// razor语法源代码 string razortxt="<div>@DateTime.Now.ToString()</div>"; // 生成的cs代码 namespace RazorService { public class Template : TemplateBase { public async override global::System.Threading.Tasks.Task ExecuteAsync() { WriteLiteral("<div>"); Write(DateTime.Now.ToString()); WriteLiteral("</div>"); } } }
razor源码转化成了Template类,只有一个ExecuteAsync方法.但这个方法里还包含WriteLiteral()和Write()两个方法,WriteLiteral的参数就是源代码中的html文本,而Write的参数是源代码中的c#代码.
但这个类本身并没有实现具体功能,看到override就表明,需要写一个基类让Template类继承它,那么,Template类使用的其实是基类的方法,除了ExecuteAsync()外.这样,基类里面就要定义并且实现WriteLiteral()和Write().
那么,RazorApi功能的实现做法就比较清楚了.一个razor文件生成一个c#类:Template,有一个方法ExecuteAsync(),执行这个方法就得到输出文本.
这个方法是override的,那么Template需要继承一个类.示例中的TemplateBase类还有类所在的命名空间RazorService,是作为参数传入的,可自定义.
模板基类访问限制要public,方法也需要public,否则继承类在使用基类方法时就会报错,即使命名空间相同.
// TemplateBase基类,抽象类,用于被Template继承 public abstract class TemplateBase { // 输出内容缓存容器 public readonly StringBuilder buffer = new(); // 具体由Template类实现 public abstract Task ExecuteAsync(); public void WriteLiteral(string literal) { buffer.Append(literal); } public void Write(object obj) { buffer.Append(obj); } ... }
Asp.NET里的Razor提供的主要功能,是编译使用Razor语法编写的文本文件,生成html文档.
Razor语法支持的指令并不完全一样,这和使用Razor语法的场景有关.早期的Asp.Net MVC和现在的ASP.NET Core Blazor支持的Razor指令大不相同.这个也就是说,可以根据需求自定义需要的功能.
使用最多的Razor功能,母版页@Layout="layout.cshtml",节@section{},引用页@include(),数据实体@model 如果要实现这些基本功能,那么基类中要实现对应的方法和属性.
// TemplateBase基类,抽象类用于被Template继承 public abstract class TemplateBase { // 母版 public string Layout { get; set; } = string.Empty; // 主页 public string Body { get; set; } = string.Empty; // 字典数据 public dynamic ViewBag { get; set; } = new ExpandoObject(); // 数据实体类 使用dynamic可以支持强类型和ExpandoObject动态类型 public dynamic Model { get; set; } = null; // 节定义 public void DefineSection(string key, Action action) // 节载入 public string RenderSection(string key, bool isRequire = false) // 页引用 public string Include(string path) ... }
目前RazorServicer项目实现了这几个基本功能,用于基本的html文本生成和其它使用razor语法的文本生成.
从调用RazorServe.Run(path)开始,读取Razor源文件.重载RunTxt(content),读取Razor文本.
对于文件路径参数,默认cshtml扩展名,可以只写文件名字,将从程序运行目录和指定的razor目录搜索文件.也可以是全路径名字,没找到文件将报错.
// 设置cshtml文件搜索目录 RazorCfg.AddSearchDirs("razorFiles"); // 开启定时器,这个方法需要调用一次,会建立缓存目录 RazorCfg.StartTimer(); // razor文件路径 string main = "main"; // 实体类 PersonEntity model = new() { Id = 1, Name = "mirror", Description = "software worker", Money = 10 }; // 实体类2 dynamic model2=new ExpandoObject(); model2.Id = 1001; model2.Name = "anne"; model2.Description = "data engineer"; model2.Money = 50; // 结果 string html = RazorServe.Run(main, model);
将从内存缓存查找,再从文件缓存目录查找,否则生成新的cs源码.首次生成的cs源码会加入缓存.
同上,从缓存查找,否则编译新的dll程序集.编译是最费时间的步骤,所以要建立缓存.
使用反射取得类和方法执行,得到文本结果.如果模板是单一的,没有使用layout和include或者section,那么只需要执行一次就得到结果了.这是最简单的情况,一个razor页生成一个Template类.
如果主模板使用了layout/Include和section,那么,layout和include引用的页面就会再次生成,这些模板其实也是一个独立的razor文件,所以会生成对应的Template类.最后,主模板引用到的所有其它页都生成后,将结果拼装到最终的输出页面,就是layout页面.
所谓的layout/include的实现,其实就是多个Template类的执行结果的组合而已.
@Html.Raw(),这个在Asp.Net MVC中经常用到,用于在页面上原样输出html标记.
使用了简单的实现办法,添加一个类HtmlExt,包含Raw()方法,然后在模板基类里加入这个类的引用,这样就实现了.
public abstract class TemplateBase { public readonly HtmlExt Html = new(); } public class HtmlExt { public string Raw(string htmlTxt) { return HttpUtility.HtmlEncode(htmlTxt); } }
// 设置razor文件的查询目录. 默认程序运行目录 RazorCfg.AddSearchDirs("razorFilesDir1","razorFilesDir2"...); // 设置缓存文件目录. 默认运行目录下的RazorTemplateCache目录 RazorCfg.CacheDir = "RazorTemplateCache"; // 内存缓存过期分钟数. 默认120分钟 RazorCfg.DeadMinutes= 120; // 文件缓存过期天数. 默认10天,超时不访问将删除 RazorCfg.DeadDays = 10; // 清理文件缓存的定时器.开启/关闭.默认没开启,需要调用 RazorCfg.StartTimer(); RazorCfg.StopTimer();
发现症状: 如果razor文档中,有包含空属性值的标记,那么编译出的razor源码,不会将这情况视为普通HTML标记直接输出,而是编译成如下代码.
// 例如html标记有一个属性class,但值是空字符串"" <br class="" /> // 编译如下 BeginWriteAttribute("class", " class=\"", 132, "\"", 137, 0); EndWriteAttribute();
这个问题折腾了2天,还发了issue,最后发现编译成这样没有问题.这两个方法也是razor工具集里的正常方法.https://learn.microsoft.com/zh-cn/dotnet/api/microsoft.aspnetcore.mvc.razor.razorpagebase.beginwriteattribute?view=aspnetcore-7.0
那么,和WriteLiteral()还有Write()方法一样,这两个方法也是razor的TemplateBase基类需要提供的方法.这个问题的原因就在于没有实现这2个方法,而以前也没有测试过razor对空属性值的这种编译规则,所以想当然的以为是语法编译错了^_^.
属性标记的相关方法: // 开始写出属性 public virtual void BeginWriteAttribute (string name, string prefix, int prefixOffset, string suffix, int suffixOffset, int attributeValuesCount); // 结束写入属性 public virtual void EndWriteAttribute (); // 写出属性值 public void WriteAttributeValue (string prefix, int prefixOffset, object? value, int valueOffset, int valueLength, bool isLiteral);
根本原因还是对razor编译法则的基础了解不够,只是生搬硬套了参考项目的代码.相信这种情况只是一个开始,随着使用的增多,应该还会有类似的没被测试到的编译规则.
可以使用匿名类做数据模型传给页面.实现上是通过转换匿名类为动态类.但转换的动态类还是有问题,对于简单的匿名类有效,复杂的依然不行.但RazorEngine项目就可以使用匿名类型,多复杂的都能,可能因为它的转换方法更好.
// 这种简单属性值类型的可以 var model=new {name="mirror",money=200} // 复杂的不行了,比如属性值是列表或者字典或者其它引用类型 var model= new {value1=List,value2=Dict,}