Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does RazorEngine compile templates with anonymously-typed models dynamically?

I have noticed that RazorEngine.Compile() seems to treat anonymous types differently from other types. For example, consider the following code:

public static void Main()
{
    try {
    var model = new { s = default(string) };
    RazorEngine.Razor.Compile("@Model.s.Length", model.GetType(), "a");
    RazorEngine.Razor.Run(model, "a");
    } catch (Exception ex) {
        Console.WriteLine(ex); // RuntimeBinderException (Cannot perform runtime binding on a null reference)
    }

    try 
    {
    var model = "";
    RazorEngine.Razor.Compile(@"@Model.Length", model.GetType(), "b");
    RazorEngine.Razor.Run(default(string), "b");
    } catch (Exception ex) {
        Console.WriteLine(ex); // NullReferenceException
    }

    try 
    {
    var model = Tuple.Create(default(string));
    RazorEngine.Razor.Compile(@"@Model.Item1.Length", model.GetType(), "c");
    RazorEngine.Razor.Run(model, "c");
    } catch (Exception ex) {
        Console.WriteLine(ex); // NullReferenceException
    }

        try 
    {
    var model = new Internal();
    RazorEngine.Razor.Compile(@"@Model.S.Length", model.GetType(), "d");
    RazorEngine.Razor.Run(model, "d");
    } catch (Exception ex) {
        Console.WriteLine(ex); // TemplateCompilationException (type Internal is not visible)
    }
}

internal class Internal {
    public string S { get; set; }
}

My understanding is this: Anonymous types are internal, so normally Razor wouldn't handle them. However, Razor provides special support for anonymous types by generating a dynamic template instead.

I thus have two questions: (1) Is my understanding of this behavior correct? (2) Is there any way to get razor to output a strongly-typed template for an anonymous model?

like image 795
ChaseMedallion Avatar asked Oct 05 '22 00:10

ChaseMedallion


2 Answers

I had the idea to make the Razor assembly a friend assembly to your assembly (thereby making internals visible), but that didn't work. In the Razor source code, we can see the following code (in CompilerServiceBase.cs):

 if (modelType != null)
 {
     if (CompilerServices.IsAnonymousType(modelType))
     {
         type.CustomAttributes.Add(new CodeAttributeDeclaration(new CodeTypeReference(typeof(HasDynamicModelAttribute))));
     }
 }

And then later (in TemplateBaseOfT.cs):

HasDynamicModel = GetType().IsDefined(typeof(HasDynamicModelAttribute), true);

Which is used later in the same file:

if (HasDynamicModel && !(value is DynamicObject) && !(value is ExpandoObject))
    model = new RazorDynamicObject { Model = value };
else
    model = value;

So yes - your understanding is correct. Also, we can see that making internals visible is not sufficient, since Razor treats any anonymous types as dynamic (RazorDynamicObject extends DynamicObject). You could try to patch the Razor code to not treat anonymous types as dynamic if the internals of the containing assembly are visible.

In that case, though, I think the code would have to emit a new public type that contains the same properties as the anonymous type of the model, so that the code generated by Razor can instantiate instances of that type. Alternatively, you could maybe use the hack described here in order to allow methods to return instances of anonymous types.

like image 50
Ben Reich Avatar answered Oct 10 '22 01:10

Ben Reich


Dynamic is used to allow for anonymous types. When it compiles the model, it can't reference the anonymous types because they are internal to your assembly. With that in mind, the Razor parser is trying to generate a template that inherits from TemplateBase<T> to give strongly typed support. This is why we use TemplateBase<dynamic> because it means we delegate the access to these properties to the DLR, but you lose the benefits of a statically typed class.

like image 22
Matthew Abbott Avatar answered Oct 10 '22 01:10

Matthew Abbott