adoconnection / RazorEngineCore

.NET6 Razor Template Engine
MIT License
565 stars 84 forks source link

RazorEngineCore.ObjectExtenders.IsAnonymous(this object obj) not recognizing dynamic or anonymous types #135

Open seppo498573908457 opened 10 months ago

seppo498573908457 commented 10 months ago

In my testing I noticed that when I enter dynamic typed or a proper anonymous typed model into the RazorEngineCore through some generic methods, I got error:

Unable to compile template: (35,77): error CS1003: Syntax error, ',' expected (35,80): error CS1003: Syntax error, ',' expected (35,125): error CS1003: Syntax error, ',' expected (35,80): error CS0246: The type or namespace name 'f__AnonymousType1<,>' could not be found (are you missing a using directive or an assembly reference?)

Error is thrown when calling Compile<>:

RazorEngineCore.IRazorEngine engine = new RazorEngineCore.RazorEngine();

void optBuilder(IRazorEngineCompilationOptionsBuilder builder)
{
    Assembly[] all = AppDomain.CurrentDomain.GetAssemblies();
    foreach (Assembly asm in all)
    {
        if (asm.IsDynamic) { continue; }
        builder.AddAssemblyReference(asm);
    }
}
var template4 = engine.Compile<MyTemplate<T>>(templateSource, optBuilder); // This row throws!
string result = template4.Run(tmpl =>
{
    tmpl.Model = model;
});

Line 35 in generated code:

public class Template : RazorTestLib48.RazorEngineCoreHelpers.MyTemplate<<>f__AnonymousType1<System.String,System.Int32>>

Creation of model:

  dynamic model = new
  {
      Name = "Testi",
      Age = 3
  };
// or:
    var model = new
    {
        Name = "Testi",
        Age = 3
    };

When debugging that model has a type named "<>f__AnonymousType1`2", it returns true for all the other checks in IsAnonymous:

Except for the last type.Attributes.HasFlag(TypeAttributes.NotPublic) although it has properties type.IsPublic = false and type.IsNotPublic = true.

Using a strongly typed class does not throw errors. I'm running a dotnet6 console app that references a dotnet48 library for proof-of-concept purposes. Am I doing something wrong here or has the anonymous detection gone bad?

When I run it in a clean dotnet6 console app, I get slightly different errors:

Unable to compile template: yt0ad4nu.gs1(35,20): error CS1061: 'object' does not contain a definition for 'Name' and no accessible extension method 'Name' accepting a first argument of type 'object' could be found (are you missing a using directive or an assembly reference?) yt0ad4nu.gs1(35,33): error CS1061: 'object' does not contain a definition for 'Age' and no accessible extension method 'Age' accepting a first argument of type 'object' could be found (are you missing a using directive or an assembly reference?)

Generated class definition:

public class Template : ConsoleRunner6.RazorEngineCoreRunner.MyTemplate

Again using strongly typed type works fine. To me the problem looks like 2-fold, the old dotnet 4.8 works differently with Type.Attributes and when using dynamic model, the generated code does not reflect a dynamic type.

-EDIT- Just in case someone wonders the assembly references, I could not make it work by adding no assemblies, adding only needed assemblies or adding all possible assemblies. The error message is usually the same, but sometimes different. What's written above seemed like the most stable combination.

seppo498573908457 commented 10 months ago

I might add, the results are inconsistent across runtimes and the way these methods are called. I copied the IsAnonymous() method and added it among my own code, then tested this:

  bool isAnon1 = typeof(TModel).IsAnonymous();
  bool isAnon2 = isAnon(typeof(TModel));
  bool isAnon11 = model.GetType().IsAnonymous();
  bool isAnon12 = isAnon(model.GetType());

isAnon12 is true, all else false. Inconsistencies like this are unbearable in production code.

adoconnection commented 10 months ago

Hi, may I ask you to post a sample code to reporoduce this problem, like you did in first message.

if you pass dynamic into generic method, it should be wrapped, see https://www.codeproject.com/Articles/5260233/Building-String-Razor-Template-Engine-with-Bare-Ha

IsAnonymous supposed to check the instance, not a type, in your example it will be obj.GetType().GetType()

public static bool IsAnonymous(this object obj)
{
    Type type = obj.GetType();

    return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false)
           && type.IsGenericType && type.Name.Contains("AnonymousType")
           && (type.Name.StartsWith("<>") || type.Name.StartsWith("VB$"))
           && type.Attributes.HasFlag(TypeAttributes.NotPublic);
}
seppo498573908457 commented 10 months ago

Thanks for your reply. Here is some sample code from my testing project. I first copied the decompiled IsAnonymous() into my code and added some formatting:

public static bool IsAnon(object obj)
{
    Type type = obj.GetType();
    if (Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), inherit: false)
        && type.IsGenericType
        && type.Name.Contains("AnonymousType")
        && (type.Name.StartsWith("<>") || type.Name.StartsWith("VB$"))
        )
    {
        return type.Attributes.HasFlag(TypeAttributes.AnsiClass);
    }
    return false;
}

The first statement is clear, it gets type of the obj. That's my bad, I didn't notice it at first. Then I run this much code:

dynamic model1 = new { Name = "Testi", Age = 3 };
var model2 = new { Name = "Testi", Age = 3 };
Person model3 = new Person() { Name = "Testi", Age = 3 };

bool isAnon11 = model1.IsAnonymous(); // throws 
bool isAnon12 = IsAnon(model1);
bool isAnon21 = model2.IsAnonymous();
bool isAnon22 = IsAnon(model2);
bool isAnon31 = model3.IsAnonymous();
bool isAnon32 = IsAnon(model3);

But the line that says throws, throws a Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: '<>f__AnonymousType0<string,int>' does not contain a definition for 'IsAnonymous'. This is caused by the fact that you cannot call extension methods with objects that are defined with dynamic-keyword. That's also what lead me to thinking that the IsAnonymous is not supposed to be called with the object itself.

Now if I pass those models into a generic method where the generic parameter is for the model, IsAnonymous seems to work on each of the models and give correct results when called on the object, thanks for pointing it out. But it throws errors from template compile as it is in my first post. I have not previously learned that I need the AnonymousTypeWrapper and also I need to use a non-generic Template for compiling (engine.Compile<MyTemplate>(templateSource, optBuilder) instead of engine.Compile<MyTemplate<T>>(templateSource, optBuilder), inherits RazorEngineTemplateBase instead of RazorEngineTemplateBase<T>).

With this info I would suggest two things. 1) better documentation about how to use dynamic and anonymous types. There are no /// XML comments in the code? 2) Maybe automate the use of AnonymousTypeWrapper?