adoconnection / RazorEngineCore

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

Calling extension methods from template #22

Closed AndyMDoyle closed 4 years ago

AndyMDoyle commented 4 years ago

Hi,

Is there a way to call extension methods from templates?

I have an object in an external assembly, and in that assembly there is a public static class without a namespace that contains some extension methods for that object.

I want to call @Model.MyObject.MyExtensionMethod(parameter) from within the template, but I get a Microsoft.CSharp.RuntimeBinder.RuntimeBinderException exception throw with the error:

'MyLibrary.MyObject' does not contain a definition for 'MyExtensionMethod'

I have tried adding a reference to MyLibrary this way:

var template = razorEngine.Compile<RazorEngineTemplateBase<TestModel>>(content, builder =>
{
    builder.AddAssemblyReference(typeof(MyLibrary.MyObject));
});

But without any luck.

Is this achievable?

adoconnection commented 4 years ago

Sure, you need to reference extension method assembly correctly. Also you need to add @using directive, either in template body or in builder.AddUsing("Assembly2"); (will do the same)

Have a look at my test app:

image

namespace Assembly1
{
    public class Model
    {
        public string Name { get; set; }
    }
}
namespace Assembly2
{
    public static class StringExtender
    {
        public static string UppercaseFirst(this string value)
        {
            if (value == null)
            {
                return null;
            }

            if (value.Length > 1)
            {
                return char.ToUpper(value[0]) + value.Substring(1);
            }

            return value.ToUpper();
        }
    }
}

namespace ConsoleApp24
{
    class Program
    {
        static void Main(string[] args)
        {
            IRazorEngine razorEngine = new RazorEngine();
            var template = razorEngine.Compile<RazorEngineTemplateBase<Model>>(
                "Hello @Model.Name.UppercaseFirst()",
                builder =>
                {
                    builder.AddAssemblyReference(typeof(Assembly2.StringExtender));
                    builder.AddUsing("Assembly2");
                });

            string result = template.Run(instance =>
            {
                instance.Model = new Model()
                {
                    Name = "andy"
                };
            });

            Console.WriteLine(result);
            Console.ReadKey();
        }

    }
}
AndyMDoyle commented 4 years ago

Perfect, thank you.

We had a custom razor engine setup that was utilizing the non-generic overrides to generate the template and this appears to be the cause. As the model is wrapped as an anonymous type it appears to affect the template generation, particularly in that with the anonymous model we found we had to specifically cast the properties of the model like this:

@(((MyLibrary.MyObject)Model.Value).MyExtensionMethod())

However I have added generic support to our custom engine so we can pass a strongly typed model and cut out this casting in the markup.

Thanks again.

adoconnection commented 4 years ago

You welcome.

For my CMS I prefer to add helpers in TemplateBase, imo it makes cshtml code look clean and nice.

pageData, FileUrl and Prettify are TemplateBase members and I dont use Model keyword at all since it does not add any value for me.

<div>
    @foreach(var document in pageData.documents)
    {
           if (document.file == null)
           {
                  continue;
           }

           <a href="@FileUrl(document.file)">document.title</a>
           <div>@Prettify(document.summary)</div>
    } 
</div>

helpers like Prettify work with dynamic values and do various type and value checks inside, so it is pretty safe to call helpers with whatever arguments