Razor Class Library and Umbraco with runtime compilation

Hi there :waving_hand:

So I’ve been experimenting with extracting my Umbraco pages and BlockList components view files to separate Razor Class Library(RCL) project.
This seems like a feature that should be supported and just work, but I’ve ran into some odd issue with local development.

I of course want to have my .cshtml files rebuilt at runtime so I can work on the frontend without recompiling the whole project.
This features works flawlessly if the views are placed directly in the Umbraco project like it is at default.
However, all views in my RCL project are not recompiled and not recognized by Umbraco.
This means I can add or reference the .cshtml files from Umbraco, at all, which break this workflow.
I’ve checked the ASP.NET docs which states some configs can be done to achieve this, but I can’t get it to work.

To ensure that the projects are actually loaded in development, I checked with my debugger and pulled some data from the assembly using reflection and can confirm that the project and their DLLs are loaded by Umbraco.

Now again, this is only really a problem in development, which leads me to believe this might be some weird config or setting that I might have missed.
uSync also can’t find the template files, except for in production.

When I build/publish for production the views are all available to Umbraco.

Here is my project structure:

The Umbraco Views folder only contains this:

I’m developing on x64 bit Linux but experience the same on a M2 Macbook with ARM.
Docker containers are all x64 bit Linux using the official asp.net docker images.
Only tested Umbraco 15.

Any help, guidance or recommendation is highly appreciated :folded_hands:

I realize this might be a bit hard to answer if you don’t know and have no code.
So I quickly made a demo project on GitHub if it helps anyone.

This may help explain a little

Umbraco 10 - Razor Class Library Packages

I’m afraid a couple of the images are broken as I had a meltdown with my blob storage :rofl: but it should exlain why it does what it does and how to get your views copied locally.

1 Like

Hi @huwred :slightly_smiling_face:

Thanks a lot for the link!

I did actually come across your page and skim through it early in my research process.
That’s also how I got a clue that this might be possible.

However, if I understand your post correctly, you move the files to the main project:

“…this only works if the files physically exist in the website’s views folder”

This is what I really would like to avoid, and my interpretation of the Microsoft docs, makes me believe it should be possible.
Furthermore, since I know that Umbraco, uSync etc. can correctly locate the files in production mode, why wouldn’t we also be able to achieve the same in development.

So while it for sure would work by copying files, what I pray for is a solution that is almost invisible to any developer and “just works” with minimal finagling.

So there’s definitely something funky going on here.

I’ve now tested with a plain asp-net mvc app and while I can’t get Runtime Compilation to work out of the box, Hot-Reload and rendering of the views works like a charm.
Maybe some configuration in the Umbraco codebase messes with how RCL components are loaded :thinking:

I’ve also tried on V13.8.1 but that release also suffers from the “only loading in production” problem.

There’s a new branch in the test repo with the plain asp-net mvc demo:
https://github.com/IAmSaeve/umbraco-rcl-demo/tree/asp-net-rcl

I believe the issue is that for templates to work and be registered Umbraco requires them to be physically present in the Views folder, I would imagine it is to do with how they are loaded from the file system which requires the file to be present in that specific location, whereas at runtime the web app can load them from either location (views folder or RCL) due to the way .net maps them

On a publish, files from a Razor Class Library are actuallty copied to the publish folder, so it makes sense that they are present/working when deployed. But I wouldn’t be surprise if Umbraco requires the physical files. Or another option is that the files are in the wrong path in your package and Umbraco doesn’t pick them up.

An option would be to use a props file. Before Razor Class Libraries, the App_Plugin files had to be copied over to be physically present. This was done with a props file that would copy when over on build. You could use that technique. I

Thank you both for your replies :slightly_smiling_face:

@huwred If Umbraco can load from multiple locations, then surely I should be able to add more locations.
The docs for runtime compilation of RCL also states this should be possible and yet Umbraco still doesn’t resolve the folders, even with an additional file watcher defined.

@LuukPeters I don’t think that is the case anymore.
AFAIK views are compiled and made part of the DLLs now and not physically present on the filesystem as separate entries.
As for that case, I did test copying the files to the build output folder and verified their folder structure matched what I would expect
The compilation of views are also noted to be on be default in the Microsoft docs, which makes it hard to believe they wouldn’t be included in the build output:

I think I’m officially putting this on the shelf for the foreseeable future.

Some further research into the Umbraco source code, GitHub issues and experimentation seem to indicate this is just a problem there might not be a good or easy solution to.

This issue got my hopes up, but as others note, it just doesn’t seem to work anymore.

I’ve also tried to fiddle with many of the Razor view discovery feature, but to no avail.

The compilation regression with RCL in .NET 9 is also a real bummer that I don’t want to deal with:

Maybe sometime in the future I’ll revisit this in hopes that I use a ‘mostly’ plug-n-play solution.

1 Like

Might be a little late to the party but when reading this i remembered a proof of concept that I worked on in the beginning of this year.

Basically allowing for “feature folders” that looks like this:

I didn’t finish this but i did get the views runtime compilation to work outside the Views folder in a RCL by adding a PhysicalFileProvider to the options during startup:

public static class StartupExtensions
{

    public static WebApplicationBuilder AddRazorRuntimeCompilation(this WebApplicationBuilder builder)
    {
        builder.Services
            .AddControllersWithViews()
            .AddRazorRuntimeCompilation();

        builder.Services.Configure<MvcRazorRuntimeCompilationOptions>(options =>
        {
            // This is the physical path to the project root, same as "../UmbProject.Web"
            var libraryPath = Path.GetFullPath(Path.Combine(builder.Environment.ContentRootPath, "..", "UmbProject.Web"));
            options.FileProviders.Add(new PhysicalFileProvider(libraryPath));
        });

        return builder;
        
    }
}

Just exactly right now, the POC is not in a “sharable” state but if this does not help you I could try to find some time to publish something to Github.

Edit:
Oh, one thing that I forgot to mention. I have a RenderController for each Document Type that returns the part to the view. Similar to this:

public class CmsArticle2Controller : RenderController
{
    ....

    public override IActionResult Index()
    {
        var viewModel = BuildViewModel(this.CurrentPage);

        return View("Pages/Article/Article.cshtml",viewModel);

    }
}

All this is inside the RCL.
1 Like

Better late then never :slightly_smiling_face:

I tested out your proposed changes on the test repo I made.
While the AddRazorRuntimeCompilation did not do anything for me, adding controllers certainly did!

I still can’t see and edit or work with templates in Umbraco, and I can’t use uSync for them either, however, rendering actually work! :tada:
There is no weird settings, movement of .cshtml files etc. just a simple render controller.

I guess using request hijacking is just necessary to achieve this, but at least it’s somewhat possible.
Not really amazed that it has to be done this way, but it’s a lot better then not having this option.
Maybe the controllers can somehow be joined into one single generic controlle.


Here is the final steup: