adoconnection / RazorEngineCore

.NET6 Razor Template Engine
MIT License
576 stars 85 forks source link

Followed @Include and @Layout wiki But Getting Exception #44

Closed Hugozys closed 3 years ago

Hugozys commented 3 years ago

Target Framework is net48. Here's the MWE :

using System;
using System.Collections.Generic;
using System.Linq;
using RazorEngineCore;

namespace mwe
{
    public static class RazorEngineCoreExtensions
    {
        public static RazorCompiledTemplate Compile(this IRazorEngine razorEngine, string template, IDictionary<string, string> parts)
        {
            return new RazorCompiledTemplate(
                    razorEngine.Compile<RazorTemplateBase>(template),
                    parts.ToDictionary(
                            k => k.Key,
                            v => razorEngine.Compile<RazorTemplateBase>(v.Value)));
        }
    }

    public class RazorTemplateBase: RazorEngineTemplateBase
    {
        public Func<string, object, string> IncludeCallback { get; set; }

        public Func<string> RenderBodyCallback { get; set; }
        public string Layout { get; set; }

        public string Include(string key, object model = null)
        {
            return this.IncludeCallback(key, model);
        }

        public string RenderBody()
        {
            return this.RenderBodyCallback();
        }
    }

    public class TestModel
    {
        public string Name{get; set;}

        public string Age{get; set;}

        public void Initialize()
        {
            Name = "name";
            Age = "age";
        }
    }

    public class RazorCompiledTemplate
    {
        private readonly IRazorEngineCompiledTemplate<RazorTemplateBase> compiledTemplate;
        private readonly Dictionary<string, IRazorEngineCompiledTemplate<RazorTemplateBase>> compiledParts;

        public RazorCompiledTemplate(IRazorEngineCompiledTemplate<RazorTemplateBase> compiledTemplate, Dictionary<string, IRazorEngineCompiledTemplate<RazorTemplateBase>> compiledParts)
        {
            this.compiledTemplate = compiledTemplate;
            this.compiledParts = compiledParts;
        }

        public string Run(object model)
        {
            return this.Run(this.compiledTemplate, model);
        }

        public string Run(IRazorEngineCompiledTemplate<RazorTemplateBase> template, object model)
        {
            RazorTemplateBase templateReference = null;

            string result = template.Run(instance =>
            {
                // if I comment out the following if clause, it works.
                if (!(model is AnonymousTypeWrapper))
                {
                    model = new AnonymousTypeWrapper(model);
                }

                instance.Model = model;
                instance.IncludeCallback = (key, includeModel) => this.Run(this.compiledParts[key], includeModel);

                templateReference = instance;
            });

            if (templateReference.Layout == null)
            {
                return result;
            }

            return this.compiledParts[templateReference.Layout].Run(instance =>
            {
                if (!(model is AnonymousTypeWrapper))
                {
                    model = new AnonymousTypeWrapper(model);
                }

                instance.Model = model;
                instance.IncludeCallback = (key, includeModel) => this.Run(this.compiledParts[key], includeModel);
                instance.RenderBodyCallback = () => result;
            });
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            var model = new TestModel();
            var testpayload = @"

@inherits RazorEngineCore.RazorEngineTemplateBase<TestModel>
@{
    Model.Initialize();
}
    <b>@Model.Name</b>
";
            var engine = new RazorEngine();
            var compiled = engine.Compile(testpayload, new Dictionary<string, string>());

            var result = compiled.Run(model);
            Console.WriteLine(result);
            Console.ReadKey();
        }
    }
}

Here's the exception I get when I run this mwe:

D:\mwe> dotnet run .\Program.cs
Unhandled Exception: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'RazorEngineCore.AnonymousTypeWrapper' does not contain a definition for 'Initialize'
   at CallSite.Target(Closure , CallSite , Object )
   at System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid1[T0](CallSite site, T0 arg0)
   at TemplateNamespace.Template.<ExecuteAsync>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at RazorEngineCore.RazorEngineCompiledTemplate`1.<RunAsync>d__12.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at RazorEngineCore.RazorEngineCompiledTemplate`1.Run(Action`1 initializer)
   at mwe.RazorCompiledTemplate.Run(IRazorEngineCompiledTemplate`1 template, Object model) in D:\mwe\Program.cs:line 71
   at mwe.RazorCompiledTemplate.Run(Object model) in D:\mwe\Program.cs:line 64
   at mwe.Program.Main(String[] args) in D:\mwe\Program.cs:line 119

Any idea where I did it wrong? If I just fall back to the basic usage, it also works:

 var compiled = engine.Compile(testpayload);
 var result = compile.Run(model);
 Console.WriteLine(result);
 Console.ReadKey();
adoconnection commented 3 years ago

You don’t need

@inherits RazorEngineCore.RazorEngineTemplateBase<TestModel>
@{
    Model.Initialize();
}

In testpayload variable

Hugozys commented 3 years ago

Hi. Thanks for the reply. I get a little confused about this. This was me trying to reproduce the exception I met so it's a simplified version. The real template string is much more complicated but basically it will invoke member functions in the template at multiple places:

@inherits RazorEngineCore.RazorEngineTemplateBase<TestModel>
@{
  Layout = "~/_layout.cshtml"
  Model.Initialize();
}

<b>@Model.OtherMemberFunction()</b>
<b>@Model.Name</b>
// omit other code

Just wondering what are the correct format? Or it simply doesn't support these use cases? Maybe to let this minimum working example make more sense:

public class TestModel
{
    public string Name {get; set;}
    public string Age{get; set;}
    public string PrintNameAge()
   {
        return $"{Name}-{Age}";
   }
}

var testpayload = @"

@inherits RazorEngineCore.RazorEngineTemplateBase<TestModel>
    <b>@Model.PrintNameAge()</b>
";
 var engine = new RazorEngine();
 var compiled = engine.Compile(testpayload, new Dictionary<string, string>());
 var result = compiled.Run(new TestModel
{
    Name = "John"
    Age = 25
});

Apologize if I understand this incorrectly.

Hugozys commented 3 years ago

So it seems I need to override the TryInvokeMember method in AnonymousTypeWrapper to make this work. If I override the TryInvokeMember to:

  public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
        {
            var methodInfo = this.model.GetType().GetMethods()?
                .Where(m => (m.Name == binder.Name) && (binder.CallInfo.ArgumentCount == m.GetParameters()?.Count()))
                .First(m => 
                {
                    int ind = 0;
                    return m.GetParameters()?.Aggregate(true, (match, next) => match && next.ParameterType.IsAssignableFrom(args[ind++].GetType())) ?? true;
                });
            if (methodInfo == null)
            {
                return false;
            }
            result = methodInfo.Invoke(this.model, args);
            return true;
        }

I don't see that error any more.

adoconnection commented 3 years ago

Yes, AnonymousTypeWrapper does not implement functions calls (until you did it). I think this code should be added in next version.

However this code is looks odd to me:

@{
   Model.Initialize();
}

The idea was to initialize model on compiled.Run moment, not in the template. When template is being executed it is too late on initialize model.

adoconnection commented 3 years ago

I’ll implement same idea as you do in PR “in completely different way” not to make any concerns about it :)

closing issue for now