Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Isolated RazorEngine failing to pass model to different AppDomain

When I render my template without the EditHistory member, this works. However, when I add that additional member that is within my application I get an exception Could not load file or assembly 'Models, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified. Models is the project containing ContentModel, EditHistory and UserDetail.

public class ContentModel
{
    public string Html { get; set; }
    public string Title { get; set; }
    public EditHistory History { get; set; }
}

public class EditHistory
{
    public IReadOnlyCollection<UserDetail> Authors { get; set; }
}

public class UserDetail
{
    public string Name { get; set; }
    public string EmailAddress { get; set; }
}

I am wrapping ContentModel in a RazorDynamicObject as such: Razor.Run("default.cshtml", typeof(ContentModel), RazorDynamicObject.Create(cm));

As mentioned above, it works without EditHistory being present, but fails when it is.

The sandbox is set up verbatim as per how it's done at https://antaris.github.io/RazorEngine/Isolation.html

How do I get it to work with complex custom types?

Running under ASP.NET.

Edit I have created a minimal reproduction of the issue I'm facing. It's at https://github.com/knightmeister/RazorEngineIssue. If package restore fails, manually install-package razorengine.

like image 535
Sam Avatar asked Dec 03 '15 05:12

Sam


Video Answer


2 Answers

First of all; I was never able to get your GitHub-code running. The following is based on my own reproducing code.

I think that you're getting Could not load file or assembly-exceptions because when you setup the sandbox AppDomain you're setting:

adSetup.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;

This won't work in ASP.NET because assemblies are in the bin subfolder. To fix that, simply do this instead:

adSetup.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase 
                          + "\\bin";

However, ASP.NET will by default shadow copy assemblies. Therefore just doing this change will probably cause another exception:

ArgumentException: Object type cannot be converted to target type.

That's because there's a mixup between assemblies loaded in the default app domain and the sandbox. The ones in the default app domain are located in a temporary shadow copy location and the ones in the sandbox are located in the bin-folder of your web application root.

The easiest way to fix this is to disable shadow copying by adding the following line under <system.web> in your Web.config:

<hostingEnvironment shadowCopyBinAssemblies="false"/>

In addition; I think it's better and easier to skip using RazorDynamicObject and instead mark your models with [Serializable]. In fact I never got RazorDynamicObject working properly.

The rest of this answer summarizes what I did to come to this conclusion


I think that this is due to a bug or limitation in RazorEngine. (I'm not so sure about this anymore, it might very well be that shadow copying and RazorDynamicObject cannot work together)

I've spent a couple of hours trying to figure out how to get this working but I've always ended up with a security exception being thrown from RazorEngine.

There is, however, a possible workaround: Ditch RazorDynamicObject and mark your model classes as serializable.

[Serializable]
public class ContentModel
{
    public string Html { get; set; }
    public string Title { get; set; }
    public EditHistory History { get; set; }
}

[Serializable]
public class EditHistory
{
    public IReadOnlyCollection<UserDetail> Authors { get; set; }
}

[Serializable]
public class UserDetail
{
    public string Name { get; set; }
    public string EmailAddress { get; set; }
}

And do:

Razor.Run("default.cshtml", typeof(ContentModel), cm); // no RazorDynamicObject

I couldn't get your repro code running, so I created my own based on your code:

  1. Create a new Console application (Visual Studio)

  2. In the package manager console, run: install-package razorengine

  3. Copy code from your repro:

    • Line 25-38 and 43-65 from:
      https://github.com/knightmeister/RazorEngineIssue/blob/master/Global.asax.cs

    • All models from: https://github.com/knightmeister/RazorEngineIssue/blob/master/Models/Models.cs

  4. Mark models with [Serializable].

  5. Remove RazorDynamicObject

  6. To ensure that we really can render user details from the authors list, change the test template to:

    string template = "@Model.History.Authors[0].EmailAddress";
    
  7. Also, to make that template work, change Authors in EditHistory from IReadOnlyCollection<> to IReadOnlyList<>

I created a GIST with the resulting code:
https://gist.github.com/mwikstrom/983c8f61eb10ff1e915a

This works for me. It prints [email protected] just as it should.


ASP.NET will shadow copy assemblies by default and that will cause additional problems with sandboxing.

To get this working under ASP.NET you'll have to do the following changes:

  1. Disable ASP.NET shadow copying by adding the following under <system.web> in your Web.config file:

    <hostingEnvironment shadowCopyBinAssemblies="false"/>
    
  2. Append \bin to the sandbox's application base path. So in createRazorSandbox(...) do:

    adSetup.ApplicationBase = 
        AppDomain.CurrentDomain.SetupInformation.ApplicationBase + "\\bin";
    

I have tested this and it works just fine. My test project is simply:

  • An empty ASP.NET Web Application (created with Visual Studio), with install-package razorengine

  • <hostingEnvironment shadowCopyBinAssemblies="false"/> in Web.config.

  • The following Global.asax.cs:

https://gist.github.com/mwikstrom/ea2b90fd0d306ba3498c


There are other alternatives (besides disabling shadow copying) listed here:

https://github.com/Antaris/RazorEngine/issues/224

like image 90
Mårten Wikström Avatar answered Oct 14 '22 22:10

Mårten Wikström


I mostly don't use complex types but a general rule is usually that only primitive datatypes are transferred ok (my own rule, since values often get lost for me otherwise). However, when looking at some old source code I noticed I did use many complex types, but I populated them in the Controller (e.g. in Public ActionResult Index()). After some reading I think it might work if you use something similar to this (untested, MSDN source, 2nd source):

[MetadataType(typeof(EditHistory))]
public partial class ContentModel
{
   public string Html { get; set; }
   public string Title { get; set; }
   public EditHistory History { get; set; }
}

[MetadataType(typeof(UserDetail))]
public partial class EditHistory
{
   public IReadOnlyCollection<UserDetail> Authors { get; set; }
}

public class UserDetail
{
   public string Name { get; set; }
   public string EmailAddress { get; set; }
}
like image 43
Gillsoft AB Avatar answered Oct 14 '22 22:10

Gillsoft AB