1

This answer explains how to use RazorProjectEngine in a C# application. In a comment in the provided example code, it is mentioned that

"Template" is the type name that razor uses by default.

I want to compile multiple Razor templates into a single assembly, and to do that I need to be able to change the class name from Template to something else. Using the example code from the answer, how can I do that?

Merlin04
  • 1,837
  • 4
  • 23
  • 27
  • You need to share the relevant code you gave written and explain what issue you see facing. – Chetan Jul 09 '20 at 22:27
  • @ChetanRanpariya The relevant code is inside the answer that I linked. The issue is that I need to be able to change the generated class name from the default of `Template`. – Merlin04 Jul 09 '20 at 22:33

1 Answers1

1

I also had this requirement and I managed to figure out how to do it. One way would be to parse the generated C# text code and do a simple string replacement of "public class Template" with "public class MyOwnNameForTemplate". However, I think is better and safer to do it on the SyntaxTree object which has already parsed the C# code right before the compilation with Roslyn compiler. Find bellow full code:

namespace MyOwnNameSpace
{
    //you can specify @inherit in your cshtml view to inherit your custom base class. If you do that, make sure that class has some dummy methods which are used by Visual Studio when it tries to compile your .cshtml
    /*
        public virtual void Write(object obj)
        {

        }
        public async virtual Task ExecuteAsync()//this is the root method which should be called for your compiled view
        {

        }
        public virtual void WriteLiteral(string literal = null)//the compiled view will emit calls to this method to write the literal HTML. Is pity that Razor does not emit async call to this
        {
            
        }
    //bellow methods are needed by Visual Studio 
        protected T CreateTagHelper<T>()
        {
            throw new Exception("CreateTagHelper");
        }
        protected void StartTagHelperWritingScope(System.Text.Encodings.Web.HtmlEncoder htmlEncoder)
        {
            throw new Exception("StartTagHelperWritingScope");
        }

        protected Func<TagHelperContent> EndTagHelperWritingScope => throw new Exception("EndTagHelperWritingScope");
     */
    public class TestTemplateCompilation
    {
        private List<MetadataReference> GetAssemblyReferences()
        {
            bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
            bool isFullFramework = RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase);
            HashSet<Assembly> referencedAssemblies = null;
            if (isWindows && isFullFramework)
            {
                referencedAssemblies = new HashSet<Assembly>()
                {
                    typeof(object).Assembly,
                    Assembly.Load(new AssemblyName("Microsoft.CSharp, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")),
                    this.GetType().Assembly,//if you have specified a base class in your template, a class which is located in this assembly
                    typeof(System.Runtime.GCSettings).Assembly,
                    typeof(System.Linq.Enumerable).Assembly,
                    typeof(System.Linq.Expressions.Expression).Assembly,
                    Assembly.Load(new AssemblyName("netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51"))
                };
            }

            if (isWindows && !isFullFramework) // i.e. NETCore
            {
                referencedAssemblies = new HashSet<Assembly>()
                {
                    typeof(object).Assembly,
                    Assembly.Load(new AssemblyName("Microsoft.CSharp")),
                    this.GetType().Assembly,//if you have specified a base class in your template, a class which is located in this assembly
                    Assembly.Load(new AssemblyName("System.Runtime")),
                    Assembly.Load(new AssemblyName("System.Linq")),
                    Assembly.Load(new AssemblyName("System.Linq.Expressions")),
                    Assembly.Load(new AssemblyName("netstandard"))
                };
            }

            if (!isWindows)
            {
                referencedAssemblies = new HashSet<Assembly>()
                {
                    typeof(object).Assembly,
                    Assembly.Load(new AssemblyName("Microsoft.CSharp")),
                    this.GetType().Assembly,//if you have specified a base class in your template, a class which is located in this assembly
                    Assembly.Load(new AssemblyName("System.Runtime")),
                    Assembly.Load(new AssemblyName("System.Linq")),
                    Assembly.Load(new AssemblyName("System.Linq.Expressions")),
                    Assembly.Load(new AssemblyName("netstandard"))
                };
            }

            return referencedAssemblies.Select(ass => (MetadataReference)MetadataReference.CreateFromFile(ass.Location)).ToList();
        }

        public MemoryStream CompileTemplate(String razorCSHTMLFilePath)
        {
            var templateContentLines = File.ReadAllLines(razorCSHTMLFilePath);
            var templateContent = String.Join("\r\n", templateContentLines.Where(l => !l.Contains("@model", StringComparison.OrdinalIgnoreCase)));//remove @model declaration since it can give compilation error
            RazorProjectEngine engine = RazorProjectEngine.Create(
                RazorConfiguration.Default,
                RazorProjectFileSystem.Create(@"."),
                (builder) =>
                {
                    builder.SetNamespace("MyMainNameSpace");
                });

            string fileName = Path.GetRandomFileName();

            RazorSourceDocument document = RazorSourceDocument.Create(templateContent, fileName);
            RazorCodeDocument codeDocument = engine.Process(
                document,
                null,
                new List<RazorSourceDocument>(),
                new List<TagHelperDescriptor>());

            RazorCSharpDocument razorCSharpDocument = codeDocument.GetCSharpDocument();
            SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(razorCSharpDocument.GeneratedCode);
            //here starts the "magic" of replacing the class name which you need
            var rootNode = syntaxTree.GetRoot();
            ClassDeclarationSyntax classDeclarationNode = rootNode.DescendantNodes((node) => true).OfType<ClassDeclarationSyntax>().Single();//make sure you don't have declared class inside your .cshtml view
            var classNameToken = classDeclarationNode.DescendantTokens().Single(t => t.Kind() == SyntaxKind.IdentifierToken);//get the original class declaration token
            var newClassNameToken = SyntaxFactory.Identifier("MyCustomClassName");//specify you class name
            var newClassDeclarationNode = classDeclarationNode.ReplaceToken(classNameToken, newClassNameToken);
            var newRootNode = rootNode.ReplaceNode(classDeclarationNode, newClassDeclarationNode);
            syntaxTree = syntaxTree.WithRootAndOptions(newRootNode, syntaxTree.Options);//replace the original syntaxtree with new one 

            List<MetadataReference> assemblyReferences = GetAssemblyReferences();//implement correctly GetAssemblyReferences method
            CSharpCompilation compilation = CSharpCompilation.Create(
                fileName,
                new[]
                {
                    syntaxTree
                }, assemblyReferences,
                new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

            MemoryStream memoryStream = new MemoryStream();

            EmitResult emitResult = compilation.Emit(memoryStream);

            if (!emitResult.Success)
            {
                List<Diagnostic> errors = emitResult.Diagnostics.ToList();

                throw new AggregateException(errors.Select(er=>new Exception(er.ToString())).ToArray());
            }

            memoryStream.Position = 0;
            return memoryStream;
        }
    }
}
  • Thanks, I figured out another way to do this but forgot to add an answer to this question. An example of it is here: https://github.com/Merlin04/RazorEngineCore/blob/multiple-templates-in-assembly/RazorEngineCore/RazorEngine.cs#L183 It uses the `ConfigureClass` method of the builder in `RazorProjectEngine.Create`. Your method is probably more flexible though. – Merlin04 Feb 01 '21 at 04:15