Thursday, June 23, 2011

Precompile your MVC Razor views using RazorGenerator

Click here to find all the posts relating to the Razor Generator A while back, I blogged about a single file generator I wrote that can precompile Razor helpers. A bit later, Chris van de Steeg started from that code base and extended it to support precompiling MVC views (see his post).

On my side, this project stayed dormant for a little while, but recently we started extending it to support a number of interesting new scenarios, including precompiling MVC views (albeit with a slightly different approach from Chris's).

Most of the new code was written by Pranav Krishnamoorthy, a dev on the ASP.NET team. Many thanks to him for moving this project forward.

Installing the generator

It’s on the VS extension gallery, so install it from there. It’s called “Razor Generator” (not to be confused with “Razor Single File Generator for MVC”, which is Chris’).

image

Walkthrough to precompile MVC views

You can use it to precompile MVC views either in a separate library or in the MVC project itself. I’ll demonstrate the separate library case, as it’s a bit more interesting.

To begin with, create a new MVC 3 app using Razor (and the ‘Internet Application’ template). Then add a new class library project to the solution (e.g. call it MyPrecompiledViews), and add a reference to it from the MVC project.

Update (4/26/2012): the best approach is to actually create an MVC project for that library, instead of a library project. You'll never actually run it as an Mvc app, but the fact that it comes with the right set of config files allows intellisense and other things to work a lot better than in a library project. See http://razorgenerator.codeplex.com/ for latest info.

Now the fun part begins: using NuGet, install the RazorGenerator.Mvc package into your class library. This adds a number of things to the project:

  • A reference to RazorGenerator.Mvc.dll, which contains the view engine
  • Logic to register the view engine using WebActivator (in App_Start\PrecompiledMvcViewEngineStart.cs).
  • Two web.config files that are there to make intellisense work while you author your views (they’re not used at runtime)
  • A sample view, which you can later remove

Let’s take a closer look at that sample view:

image

image

Notice that it has a Custom Tool set to RazorGenerator, which causes it to generate a .cs file underneath itself (thanks to the generator we installed earlier).

This is just a sample, so now let’s move the Views\Home\Index.cshtml from the MVC project to the same folder in the class library (you can press Shift during the drag/drop to make it a move). Then set the generator to RazorGenerator as in test.cshtml. You’ll now get an Index.cs nested under Index.cshtml.

And that’s it you’re done! You can now run your app, and it will be using the precompiled version of Home\Index.cshtml.

Why would you want to do that?

One reason to do this is to avoid any runtime hit when your site starts, since there is nothing left to compile at runtime. This can be significant in sites with many views.

Also, you no longer need to deploy the cshtml files at all, resulting in a smaller deployment file set.

Another cool benefit is that it gives you the ability to unit test your views, which has always been something very difficult with the standard runtime compilation model. I’ll cover that in more details in a future post.

Generating files at design time vs. build time

The way the generation works is very similar to T4 templates you have you project. The generation happens as soon as you save the file. You can also force it to regenerate by right clicking on the .cshtml file and choosing Run Custom Tool.

Generally, the guidance is to commit those generated files along with the cshtml file, the same way that you commit all your ‘hand-written’ source files. If you do that, everything will run just fine in an automated build environment.

Another reason to commit the generated files is that it allows you to write code against them with full VS intellisense. e.g. if you use this technique to write Razor helpers that you want to call from other views, you really want VS to know about the generated file at design time. Ditto if you want to write unit tests against your views.

That being said, if you really want to postpone the generation until build time, we’re working on an MsBuild task that will do that. For now, you can find it by getting the RazorGenerator sources on CodePlex.

If you want to help or report issues

This project is hosted on http://razorgenerator.codeplex.com/ under the Ms-PL Open Source license, so feel free to contribute! You can also use CodePlex to discuss and report issues.

79 comments:

  1. Great work David & Pranav!

    ReplyDelete
  2. Looks great, but what about the command line scenario? Or is it handled automatically by msbuild?

    ReplyDelete
  3. Good question does this work in an automated build environment?

    ReplyDelete
  4. The MsBuild task was added very recently, so it still needs polishing. It does the same things that the Single File Generator (SFG) does, but differs in that unlike the SFG, compiled files are dropped to a temporary directory, and used to generate the binary but not added to the project. This allows you to come up with a workflow where the only files checked in to your source tree are the cshtml files.

    The msbuild task only has dependencies on Razor and Mvc assemblies, so you should be able to use it in a CI as any regular msbuild task.

    ReplyDelete
  5. Can we assume this will be added to future releases of MVC as default?

    ReplyDelete
  6. @kotuadam: right now it's still experimental, but if it picks up and the demand is there, I would think it could find a place in MVC eventually.

    ReplyDelete
  7. @Jeff & @Brett: I just added a new 'Generating files at design time vs. build time' in the post.

    ReplyDelete
  8. Great news. I am looking forward to get an MSBuild task for that, which could be injected into MSDeploy pipeline.

    ReplyDelete
  9. David this is a very interesting project I'm going to have to check this out soon. I can't see much value in defering from design time to build time however I'm also a person that commits all my binaries into mercurial.

    Also random note there is never a time its correct to make the possesive chris' it will always be chris's, I know this from it being my name. The only time it would be left with the trailing ' is if you were talking about multiple people named chris working on a project which would then be "the chriss' work"

    ReplyDelete
  10. @dotnetchris: thanks for pointing this out! Fixed now :)

    ReplyDelete
  11. i'm against encouraging modifications to the generated .cs file. With this we might end up having same problems with web forms' code behind?

    ReplyDelete
  12. @BumbleBee: not quite following you. Nothing here is about encouraging modifications of generated files. It is almost always a bad idea.

    ReplyDelete
  13. This is awesome. Will work great in the Shrink-Wrap apps scenario. But doing it for individual files is too much work. We are awaiting your MSBuild Task version :)

    ReplyDelete
  14. Can you tell me, what are the differences compared to Chris's version?

    ReplyDelete
  15. I can't find the extension on the extension gallery. I'm guessing it doesn't work with Web Developer right?

    ReplyDelete
  16. @webdesign: what do you mean by 'doing it for individual files is too much work'? Once a view is set up with the generator, there is nothing special you need to do for it to work. Or are you referring to the act of setting the Custom Tool being painful? We do need to make this easier.

    ReplyDelete
  17. @fran: one difference is that this uses an MVC View Engine while he uses an alternate implementation. Of course, you can view this as an implementation detail. Also, the use of a nuget package removes a number of set up steps.

    ReplyDelete
  18. @Sérgio: indeed, the generator won't work on Express. Generally, Express doesn't allow installing VSIX extensions.

    ReplyDelete
  19. Don't you just love the fantastic world of .NET where we have to spent loads of time doing all these wonderful things that in all honestly don't directly provide direct customer value but have to be done? Crap like this should be baked into the environment for starters...

    ReplyDelete
  20. @Gabriel: not sure I understand your comment. Do you mean that you'd like the ability to precompile views to be part of MVC and not an extension. That would be good feedback, but please clarify :)

    ReplyDelete
  21. This is really neat, and I like this functionality, but I think this really ought to be native to the entire MVC system. I appreciate all of the work you guys have done to make this, but this shouldn't be something we need an extension for, in my opinion. With any hope, they will extend MVC 4.0 to allow pre-compiled views out of the box.

    ReplyDelete
  22. @Stacey: at this point, it's new and experimental. If the feedback is positive and enough people are interested, it's conceivable that it would make its way into the core MVC offering at some point.

    ReplyDelete
  23. Seems great.

    Thinking this might be very usefull to use razor views as email templates, if it is possible to easily execute the view and get back the html ?

    Will it work with layout/masterpages ?

    ReplyDelete
  24. @Martin: see my following post on unit testing: http://blog.davidebbo.com/2011/06/unit-test-your-mvc-views-using-razor.html

    ReplyDelete
  25. Would it be possible to use this to create "pluggable" areas? I'd love it if I could add an area to an MVC app by deploying an assembly. Using the stock functionality, it appears that the views must be under the same Views folder as all other views, which could result in name collisions if I had several assemblies containing precompiled views.

    ReplyDelete
  26. @Andy: yes, I played with that today and it seems to work well. Check out the sources (from http://razorgenerator.codeplex.com/) for a working sample of an Area defined in a separate assembly

    ReplyDelete
  27. Thanks.

    For some weird reason it is not possible to find T4MVC via Nuget. I can see it on the Nuget wbsite, but when i search via the VS Manager it is not possible to find it.

    ... or maybe I am the only one with the problem?

    ReplyDelete
  28. @Martin: that seems unrelated to the Razor Generator. But I don't see this behavior. Please follow up on the NuGet forum to keep this post focused :)

    ReplyDelete
  29. Another nice use for your framework is to generate static html of all your views. This is helpful when working with a designer who doesn't use VS. You send the designer your generated output, they tweak it and return to you, you update your Razor code and regenerate. Pretty nice workflow!

    ReplyDelete
  30. I did the whole step and no .cs files are being generated.

    ReplyDelete
  31. Also if you do this you have to move your models to the same project as the other views because that'll make a circular reference with typed views

    ReplyDelete
  32. @Brad: I've never seen a case where nothing gets generated and there is no error. In my experience, when you set the custom tool, either you get an alert that it doesn't know the tool, or it generates the file. If you have a consistent repro, please file a bug on razorgenerator.codeplex.com so we can investigate further.

    ReplyDelete
  33. @David so apparently I am getting a warning, not an error, that the RazorGenerator cannot be found on this system, so it doesn't seem that NuGet worked.

    ReplyDelete
  34. @Brad: NuGet will not get you the razor generator. Instead, use the VS extension gallery to get it. See info in the post above :)

    ReplyDelete
  35. @David already did that, I followed every single step in the whole article, twice ;/

    ReplyDelete
  36. Strange, let's take this to codeplex so we can discuss and investigate more properly.

    ReplyDelete
  37. This comment has been removed by the author.

    ReplyDelete
  38. I am running on Windows 7 64 bit. I followed all your instructions but RazorGenerator will not run. When I try to run it via "Run Custom Tool" on the cshtml file I get the following error: "Cannot find custom tool 'RazorGenerator' on this system"
    Any ideas?

    ReplyDelete
  39. @Patricio: strange, it should work. Can you start a discussion thread on http://razorgenerator.codeplex.com/? We'll help you investigate from there. Thanks!

    ReplyDelete
  40. David, thanks for the quick response.

    I happened to figure out what the problem was. I added the Razor Generator extension into VS and got confused because the description mentioned "Single File Generator", so I thought I had gotten the wrong one and proceeded to uninstall it. After I uninstalled, I re-installed it. This workflow left some files on my machine, under C:\Users\patricio\AppData\Local\Microsoft\VisualStudio\10.0\Extensions\RazorGenerator contributors\Razor Generator\1.1.1 that had an extension "shoulddelete" or something to that manner. I had to remove them by hand to get the tool to work. Maybe there is some bug in the uninstall logic.
    In any case, I am up and testing pre-compiled views.

    Thanks again.

    Patricio

    ReplyDelete
  41. @Patricio: strange, seems VS got confused. 'Single File Generators' is what those things are called, but I can see it being confusing. Maybe I will change the description. :)

    ReplyDelete
  42. Hey David.

    I should wanted to let you know that your and Chris van de Steeg's efforts on this front have made my life, from a re-usability of views across many websites point of a view, really quite a pleasurable experience.

    Cheers,

    Alex.

    ReplyDelete
  43. @Alex: thanks for the good feedback! :)

    ReplyDelete
  44. Hi David,

    Thanks for your tutorial. This was just what i'm looking for! I successfully placed my views in the separate class library and MVC can resolve them. But i can't figure out how to override them from my project. I placed A view in /Views/(model)/(viewname).cshtml, but it is not loaded by MVC. Instead, the one from the class library is shown.

    This is what i did:

    1. Create MVC3 project
    2. Create class library
    3. Added precompiledmvcviewengine reference to class lib
    4. installed razorgenerator
    5. set build action of all views to "RazorGenerator"
    6. Added references to the web project
    7. copied web.config files

    What am i missing?

    ReplyDelete
  45. @Simon-Paul: in the App_Start code, try changing the logic so the precompiled engine gets registered into ViewEngines.Engines at the end instead of the beginning. Have not tried this, so let me know how that works for you!

    ReplyDelete
  46. @David: i added the following function to global.asax.cs and called it in Application_Start():

    public static void RegisterViewEngines(ViewEngineCollection viewEngines)
    {
    viewEngines.Clear();
    viewEngines.Add(new RazorViewEngine());
    viewEngines.Add(new PrecompiledMvcEngine(typeof(PrecompiledMvcViewEngineStart).Assembly));
    }

    Works great! Thanks!

    ReplyDelete
  47. @Simon-Paul: glad to hear it! I'm guessing you could skip the first two lines since it should already be in that state by the time you get called. So you just add the new one at the end.

    ReplyDelete
  48. @Simon-Paul: You could just change the line in PrecompiledViewEngineStart from ViewEngines.Engines.Insert(0, engine) to ViewEngines.Engines.Add(engine). Works for me!

    @David: thanks for this, this is great! I've spent all day trying to figure out how to have my views in a separate assembly before finally stumbling across this. Hope to see it included in a future release of MVC.

    ReplyDelete
  49. @Stewart: thanks for the feedback! Agreed that it would make sense to get this into an MVC release at some point.

    ReplyDelete
  50. @David,

    Great project. I have one issue. When I place my views into a separate project, I lose IntelliSense for any namespaces outside of the core MVC namespaces.

    For example, I have MvcMiniProfiler, and I cannot get IntelliSense in my .cshtml files in my class library, even though MvcMiniProfiler has been added to my class library. Even if I add the MvcMiniProfiler namespace in the Views folder web.config file, I still do not get IntelliSense.

    If I leave my views as part of my main MVC application, IntelliSense works as expected.

    Any advice?

    counsellorben

    ReplyDelete
  51. @counsellorben: would you mind starting a discussion about this issue on http://razorgenerator.codeplex.com/? It'll make it easier to discuss than using this post's comments. Thanks!

    ReplyDelete
  52. @counsellorben: seems like a very similar discussion was just started here: http://razorgenerator.codeplex.com/discussions/268455. So let's use that one.

    ReplyDelete
  53. Thanks a lot. This is a Great Project!

    One question here. I think I can also compile the Layout.cshtml but how would you set a compiled Layout at _ViewStart.cshtml when the Property Layout is expecting a path? Thanks!

    ReplyDelete
  54. You still set the path the same way as it if wasn't precompiled. Generally, you don't need to change the code when you precompile the views. Please follow up at http://razorgenerator.codeplex.com/ if you have further questions, which will make it easier to discuss. Thanks!

    ReplyDelete
  55. Thanks for this, it's a great project! I love the way controllers just *work* as well from the class library..

    One question though - when constructing precompiled views in my class library, it doesn't recognise the @model keyword, so everything comes through as dynamic type. Is there a way to get this to work?

    ReplyDelete
  56. @Steve: this should work. Could you file a bug on http://razorgenerator.codeplex.com/ with more details about what you're trying and what you're seeing? Thanks!

    ReplyDelete
  57. nice! but...

    if i have mvc project with _viewStart saying that layout should be "~/Views/Shared/_Layout.cshtml" and im using library with precompiled views. is it possible to override layout in precompiled views?

    ReplyDelete
  58. @max: yes, you should be able to include _viewStart in precompiled libraries and have them behave as normal. If you run into issues, please post to http://razorgenerator.codeplex.com/

    ReplyDelete
  59. David,

    Tried the steps outlined above, but the installation process only worked partially. Not everything outlined in your list got created.

    These are the items that did not get created:

    -- Two web.config files that are there to make intellisense work while you author your views (they’re not used at runtime)
    -- A sample view, which you can later remove

    Tried it seveal times starting from scratch each time (ie. making sure the extension files and the nuGet files were deleted before trying to install them again).

    Any suggestions?

    Thanks,

    Fausto

    ReplyDelete
  60. David,

    Since the original instructions were not working for me, I decided to skip the "Extension Gallery" portion of the instructions (after having deleted all references to extensions and nuGet packages and dlls), I went ahead an did a direct nuGet for "RazorGenerator". The same result as before (view and web.configs missing). Just for kicks and giggles (and because it has a name similar to the file in the "App_Start" folder) I added "PrecompiledMvcViewEngine" from nuGet, and that automatically added what was missing.

    Still not working though, as I get the "Cannot find custom tool 'RazorGenerator' on this system" warning when the "Custom Tool" property is set to "RazorGenerator".

    Not sure what it all means (if anything at all), just thought I'd include it for information's sake.

    Thanks,

    Fausto

    ReplyDelete
  61. @Fausto: could you please start a thread on http://razorgenerator.codeplex.com/ so we can discuss and get to the bottom of it? Thanks!

    ReplyDelete
  62. Excellent work. Blazing performance.
    Love it !

    ReplyDelete
  63. It's not working in Medium trust though. How can I make it happen ? Thanks in advance.

    ReplyDelete
  64. Hi David,
    I'm not able to install Razor Generator on VS 11 Beta, as there is no such extension.. Could you please advise?

    ReplyDelete
  65. @kaatula: yes, it's a known issue. See http://razorgenerator.codeplex.com/workitem/54 for details and updates.

    ReplyDelete
  66. @David: I downloaded your example to use the views as an external library and works like a charm but when I try to replicate the example on my project it doesn't work. Do I need to do something else besides:

    1. Downloading and installing the nuget pkg (on my views library project which was creating the mvc application template)
    2. Setting the CustomTool to RazorGenerator on the view files
    3. Referencing on my main site the external views library project

    Its worth mentioning that I'm using ASP.NET MVC 4 in this scenario.

    ReplyDelete
  67. @Raul: please use the forum on http://razorgenerator.codeplex.com/ to discuss issues. It tends to work better than blog comments.

    ReplyDelete
  68. Can precompiling views be done at runtime? I am working on a project where the end user will be able to modify views stored in the database. What I am wondering is if I can have some kind of "publish" process pull them from the database and pre-compile them into the application. Also, it would be helpful to know if it is possible to "publish" views in the production site this way whether the site would have to be taken offline to do so.

    ReplyDelete
  69. @id: please use http://razorgenerator.codeplex.com/ for all RazorGenerator related discussions. Thanks!

    ReplyDelete
  70. Hi David,

    Thanks for such a nice solution.

    How I can register controller of precompiled into my main application, so that I can access through URL by typing like /controller/action .
    Let's I have 'TestController' in precompiled view MVC project with action method say 'sample'.
    Intend is to type something like "http://localhost:21685/Test/sample" in URL or set into MapRoute.

    Thanks in advance,
    Akshay Dubey

    ReplyDelete
  71. @Akshay: please use http://razorgenerator.codeplex.com/ for all RazorGenerator related discussions. Thanks!

    ReplyDelete
  72. HI David, thanks for such a great Post. I have gone through many different approaches to achieve plug-gable nature, but yours is the easiest way. I kinda got stuck on one thing, how do I have specific lay out for each dll. As of now each reusable dll is taking the main web application's layout.

    ReplyDelete
  73. @Pavan: it's best to post all questions to http://razorgenerator.codeplex.com/ instead of here. Thanks!

    ReplyDelete
  74. How to use compiled views by using dll files from razorgenerator tool to another projects.

    Plz help me me me.....

    ReplyDelete
  75. @Rajesh: it's best to post all questions to http://razorgenerator.codeplex.com/ instead of here. Thanks!

    ReplyDelete
  76. Hi David,

    Thanks, for your quick reply

    I have made the separate library by razor generator tool and getting the dll files.

    After that i want to access those views inside the dll files to my main mvc projects. I have study carefully this url http://razorgenerator.codeplex.com/. but i can't got the answer.
    Note - I am using MVC4 not MVC3.

    if possible to send me your Skype id or mail id for doing some chat with you.

    Thanks!!

    ReplyDelete
  77. @Rajesh: I didn't mean that you would directly find your answer on the forum, but that it is the right place to post such questions instead of here. Much easier to go back and forth, and others can see it too.

    ReplyDelete