Question

I have a class library that uses RazorEngine to generate emails. RazorEngine uses Mvc. I have referenced System.Web.Mvc in my library class, set it to copy local, and it is present in the Bin folder. However I still get this exception.

The only way I got it working, is to actually call something from the Mvc namespace like MvcHtmlString test = new MvcHtmlString("test"); before calling RazorEngine (solution found here)

I guess it's not the end of the world, but I would still like to resolve this problem (or at least understand what's causing it). Cheers

Was it helpful?

Solution

OK, so I was able to boil the problem down to the following: You have something like a self-hosted WebAPI/console application, and want to use Razor to create HTML pages. Those Razor templates use some classes from Sytem.Web.WebPages or System.Web.Mvc or the like, which are bundled in their own assemblies.

But those classes are used only in the views. When starting in debug mode you will see that the runtime does not load those assemblies, regardless if they are added to the project references or not. It appears when compiling razor files, the compiler sees only namespaces from loaded assemblies, so you get those X-not-found exceptions. You will have to load the needed assemblies manually (and make sure a compatible version is found).

One short example:

We have a layout.cshtml and an index.cshtml. We use some HTML helper functions to create a form. layout.cshtml:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
</head>
<body>
  <div class="main-content">
    @RenderBody()
  </div>
</body>
</html>

index.cshtml:

@using System.Web.Mvc;
@{
  Layout = "layout";
}
<div class="box-body">
  <h1>Hello!</h1>
  <p>
    Got Html? @MvcHtmlString.Create("<b>FooBarBaz</b>")
  </p>
</div>

Source code where the razor files are parsed and HTML output is handled. For simplicity it is assumed all files are near the executable; you may need to adapt to your storage structure:

//load required assembly. By using the short name here the runtime uses
//the the first one that matches. See MSDN for further information
System.Reflection.Assembly.Load("System.Web.Mvc");
//cache the template before the actual page to avoid a null reference exception
RazorEngine.Razor.GetTemplate(File.ReadAllText("layout.cshtml"), "layout");
var result = RazorEngine.Razor.Parse(File.ReadAllText("index.cshtml"));
Console.Write(result);

You then need to add the assembly to your project (in this example possible via nuget, Microsoft.AspNet.Mvc) and maybe manually add the reference (if it isn't added automatically). This way you get the (hopefully) right version copied along with your executable.

One final note:

Nearly all goodies available in an ASP.Net MVC view (@Html for example) are objects injected by the ASP.NET pipeline and therefor not available when using Razor directly. (I don't know if or how it is possible to pass objects to Razor besides models) So most classes and functions from the System.Web.Mvc or System.Web.WebPages namespaces are probably out of reach (without reconstructing parts of ASP.NET).

The used @Layout and @RenderBody() are usually some of those ASP.NET helpers too. But as they are pretty common, it appears the author recreated their function manually in the RazorEngine parser.

OTHER TIPS

The RazorEngine has a class CompilerServicesUtility. This is responsible for loading and making assemblies available when your view is compiled. It by default returns all assemblies loaded in the current AppDomain.

When you instantiate any class in that assembly, the assembly is automatically loaded within the AppDomain and that is the reason instantiating MvcHtmlString test = new MvcHtmlString("test"); before invoking RazorEngine works.

It's a bit unintuitive for someone looking at the code, but another way to make sure that Razor compiles and you can use MvcHtmlString is to load the System.Web and System.Web.Mvc assemblies into the current AppDomain using AppDomain.Current.Load("<assemblyname>") prior to executing/compiling your view. Of course you could specify the list of assemblies needed in the config file and load it in a dynamic manner.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top