Isolated RazorEngine failing to pass model to different AppDomain
Asked Answered
A

2

15

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.

Arterio answered 3/12, 2015 at 5:21 Comment(8)
what happens if you replace public EditHistory History {get;set;} with public IReadOnlyCollection<UserDetail> History {get;set;} and also the same test with a simple type public IReadOnlyCollection<string> History {get;set;}? Could the read only collection be the problem? What if you used a IList or IEnumerable? Same results?Document
Any chance you can put together a simple VS project that reproduces this issue and publish somewhere (e.g. GitHub)?Claycomb
@CaioProiete - good idea. I have a feeling not a lot of people are going to be familiar with this particular module. I'm not, but am curious to track down exactly what is causing the failure (nested complex types, the read only collection, etc)Document
Hi guys - give me an hour or so and I'll put one up.Arterio
@CaioProiete I've put the project up now. See edit for url.Arterio
@Document Project now up, see edit for url.Arterio
Once I got the packages restored, I am getting this error out of the box Type is not resolved for member 'RazorEngine.Templating.IsolatedRazorEngineService+DefaultConfigCreator,RazorEngine, Version=3.7.4.0, Any thoughts?Document
@Document try manually adding the package install-package razorengineArterio
P
5

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:

  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

Provo answered 11/12, 2015 at 2:55 Comment(7)
Hi, thanks for trying. I tried doing this prior to posting, and I've just tried again with the example I posted at GitHub (see OP) - were you able to get this running?Arterio
@Sam: Please see my recent edit that shows what I did to get it running.Sheppard
appreciate your help, but this doesn't work under ASP.NET. It needs to run under ASP.NET.Arterio
@Sam: Please see my recent edit for a ASP.NET compatible solution.Sheppard
Thanks so much for your help I really appreciate it. You've put a lot of effort into this and would upvote you more than once if I could. Hopefully, overtime others will upvote it.Arterio
@Sam: You're welcome. As the author of a question you also have the option to choose one answer as the accepted answer. It's even more powerful than upvoting :-). See: stackoverflow.com/help/someone-answersSheppard
Silly me... I forgot. I did the bounty and didn't even think about marking it as answered...!Arterio
S
1

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; }
}
Savannasavannah answered 12/12, 2015 at 15:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.