Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How does one integrate F# Giraffe with Orchard Core MVC rendering? #7141

Open
willnationsdev opened this issue Sep 25, 2020 · 14 comments
Open
Milestone

Comments

@willnationsdev
Copy link

willnationsdev commented Sep 25, 2020

Trying to render Razor views using F# Giraffe.Razor. I can register endpoints to the EndpointRouteBuilder in my StartupBase-derived Startup class, but the library seems limited to actually rendering .cshtml files either from Razor class libraries or from an absolute file path.

Unfortunately, if I use IWebHostEnvironment or IHostEnvironment to get the ContentRootPath, this will provide me a path to the OC web app, not the module.

What would be the correct procedure for getting the absolute file path to the module project, so that I might provide a Views path to my rendering logic? Or, is there something else I'm missing that I should be doing?

Edit: I looked through the docs as best I could, but I didn't see any mention of how to get this information. Closest I found was the Shells documentation that talked about how to configure things from an appsettings in the local folder. There is information on how to add MVC support to your module in the Modules docs, but that kinda does all the work for full MVC rather than just letting me use the Razor view engine. I don't have any Controller classes or anything like that in F#, so I don't think that solution would work.

Edit 2: I see that I can fetch the IExtensionManager and that it provides ways to fetch info on extensions which themselves store info on a subPath, but it looks like its a relative path for Area\<extensionId>.

Edit 3: None of these StackOverflow approaches seem to work since every time I get the executing assembly's location/codebase, it always returns something related to the OC web app rather than the module (likely because the modules only provide class definitions that are pulled in to the main app?). So, no solutions found there either.

@Skrypt
Copy link
Contributor

Skrypt commented Sep 25, 2020

/cc @jtkech

Short answer is that we load modules in tenants separately. So those are Features of a tenant. So, there is some module loading business logic in there which also needs to look for module paths. You're not far but @jtkech could tell you more directly as he worked in that part a lot.

@willnationsdev
Copy link
Author

willnationsdev commented Sep 25, 2020

Oh, I think I finally found it. ModuleProjectRazorFileProvider's constructor, injects IApplicationContext and then uses it to sift through each of its modules, find .cshtml file assets, and then get its directory:

                        // Get module assets which are razor files.
                        var assets = module.Assets.Where(a => a.ModuleAssetPath
                            .EndsWith(".cshtml", StringComparison.Ordinal));

                        if (assets.Any())
                        {
                            var asset = assets.First();
                            var index = asset.ModuleAssetPath.IndexOf(module.Root, StringComparison.Ordinal);

                            // Resolve the physical "{ModuleProjectDirectory}" from the project asset.
                            var filePath = asset.ModuleAssetPath.Substring(index + module.Root.Length);
                            var root = asset.ProjectAssetPath.Substring(0, asset.ProjectAssetPath.Length - filePath.Length);

                            // ...

                            // Add the module project root.
                            roots[module.Name] = root;
                        }

I'll try giving this a shot I guess, though I need to figure out how to grab just my module's info...

Edit: Looks like IApplicationContext is the thing that actually contains the ModulePath and ModuleRoot properties, but unfortunately they are virtualized folder paths, not absolute paths. Need to find out how to convert them...

Edit 2: Well, looking at the file providers doesn't directly seem to help because they all attempt to serve files from virtual directories within the OC ApplicationContext (that is, the context's .Application.Root path is the OC app's path). So, I have to look up how the files are being registered under the virtual paths in the first place.

Edit 3: I tried injecting IEnumerable<Module> and filtering it to the module with my module name, but the module list is empty to begin with. Not sure how to get a later service configuration that executes after modules are loaded, though I know I've seen that before.

@jtkech
Copy link
Member

jtkech commented Sep 25, 2020

Yes, you are in the right place, look at the properties of application context and of a given module, and then its assets that have physical and virtual paths. Yes, we have physical and virtual paths, the virtual ones are related to the files that are embedded in the assembly of a given module, we not only embedd razor files but also liquid files, json files and so on.

In Development mode we use physical files for razor views, so that at runtime we can re-compile them on change, in Production mode we only use embedded files and their virtual paths. Normally in a prod env the razor views physical files of a given module don't exist anymore. So, even with another engine you may need to use both physical and virtual paths as we do. If at runtime you don't need to re-compile a razor view on change, you would just have to use virtual paths.

To retrieve the virtual path of a given module

var path = applicationContext.Application.GetModule("OrchardCore.Contents").SubPath;
path = applicationContext.Application.GetModule("OrchardCore.Contents").Root; // idem with a trailing slash

In dev mode, to map it to a physical path you need to use the module assets collection as we do

@willnationsdev
Copy link
Author

@jtkech Ok, thanks for the feedback! Seems relatively complex though, and I've seen other intriguing things that I have to keep note of that the MVC framework seems to just do automatically. I might need to look into the possibility of having declared Giraffe routes emulate controllers and the like, so that I can slip Giraffe HttpHandler logic into the MVC pipeline / masquerade it as such rather than attempting to replace that pipeline with custom Giraffe logic. Hmmm. We'll see if this works first.

@willnationsdev
Copy link
Author

@jtkech

So, even with another engine you may need to use both physical and virtual paths as we do. If at runtime you don't need to re-compile a razor view on change, you would just have to use virtual paths.

Looks like Giraffe.Razor just gets the IoC injected IRazorViewEngine and calls FindView(...) on it in order to locate the correct view, so, to my understanding, that would grab the same view engine that Orchard Core CMS is using.

Yes, you are in the right place, look at the properties of application context and of a given module, and then its assets that have physical and virtual paths

Yup, when I switched to injecting IApplicationContext and accessed its Modules property, I was able to filter it down to just my module, gets its assets, and find both the virtual ModuleAssetPath as well as the absolute ProjectAssetPath.

However, now I'm quite confused (perhaps because I'm somewhat new to MVC in general). If I get the absolute path, get the substring that goes to just the Views directory (and not to the actual file), and then pass that in as my views directory, any request for the view named "Index" (with either or both of Views\Index.cshtml and Views\Shared\Index.cshtml) all result in an error saying it couldn't find it and that it searched in "lists all virtual file paths". But then, if I use the virtual Views directory, I get a "must be an absolute path" error.

Nevermind, I looked more into the Giraffe.Razor's middleware code, and then I stumbled upon this genius gem:

namespace Giraffe.Razor

[<AutoOpen>]
module Middleware =

    open Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation
    open Microsoft.Extensions.DependencyInjection
    open Microsoft.Extensions.FileProviders

    type IServiceCollection with

        member this.AddRazorEngine(viewsFolderPath: string) =
            this.Configure<MvcRazorRuntimeCompilationOptions>(fun (options: MvcRazorRuntimeCompilationOptions) ->
                options.FileProviders.Clear()
                options.FileProviders.Add(new PhysicalFileProvider(viewsFolderPath))).AddMvc().AddRazorRuntimeCompilation()
            |> ignore
            this.AddAntiforgery()

Why on earth you would choose to clear all existing file providers before adding your own, I have no clue. But, once I implemented my own version of this same middleware that omits that line of configuration, my views suddenly started working!

Thanks again for all your help!

@willnationsdev
Copy link
Author

willnationsdev commented Sep 28, 2020

Welp, nevermind. I got a little ahead of myself. If I take out the clear operation and then put a Views\Shared\Index.cshtml file in my project, then attempting to render view "Index" successfully renders it at all times, even when not targeting that URL specifically. And if I use Views\Index.cshtml, then it still gives me an error that it cannot find the specified "Index" view.

Looks to me like it's actually getting to the F# logic, but the F# code can't seem to find the file for some reason (it's just passing the requested viewName to the engine).

adminmenu_giraffe_view_not_found

That error message is displayed when the configured view engine doesn't find the view here.

There's gotta be something about the way the file providers are configured that is making it miss it. Is there perhaps a way for me to confirm exactly where the IRazorViewEngine is looking, so that I can explicitly check where the paths aren't matching up?

Edit: I read on this StackOverflow question that if you are using file paths, you have to provide the actual file name, but when I specify Index.cshtml as the viewName to the F# logic, it just says it can't find it "in" <insert empty list of files>, so not sure what that's about.

@Skrypt
Copy link
Contributor

Skrypt commented Sep 28, 2020

It probably has something to do with the Themes in OC. Your views will work if they are Shared because they will be accessible in any module / themes of any project . But here, when you put your Views in the /Views folder it doesn't find it because it's contextual to the module and/or theme you are actually using in your Tenant.

I'm just saying without looking in the code too much. I would need to analyze this more to be honest. Maybe hopefully @jtkech will have a more detailed answer for you 😄

Edit : Keep posting your progress.

@willnationsdev
Copy link
Author

@Skrypt

I wonder if I can get approval to share a minimum version of my project without any proprietary stuff...will ask about that, since giving you guys an example project might make it easier to understand what I'm struggling with...? Not that I know whether or not anyone here even uses / understands how to read/write F#, lol. It's pretty under-represented in the .NET community, from what I understand.

@willnationsdev
Copy link
Author

Oh, well, crap. According to this, even if I do manage to get the views inside the project to work, none of the intellisense and tooling even works in F# projects. -_- I hadn't tested that yet and assumed it would be the same. What a waste of time going down that rabbit hole.

Guess I'll create a C# Razor Class Library to define the views and see if Giraffe.Razor can pick up those views (since the docs say that is an option rather than creating an explicit views directory path). Really annoying though...Makes me wanna write up a whole F# Fable library for doing OrchardCore stuff so I don't have to deal with it, but who has the time for that? :-P Oh well.

I'll write back here to let you know if I can get class libraries to work.

@willnationsdev willnationsdev changed the title Absolute path to module directory? How does one integrate F# Giraffe with Orchard Core MVC rendering? Sep 28, 2020
@willnationsdev
Copy link
Author

willnationsdev commented Sep 29, 2020

Well, I'm thinking that I don't necessarily want to have to write up a separate class library every time I want to put together views for a module (2 projects per feature that must be paired together is just nonsense). So, I can think of only 2 remaining alternatives.

  1. create a fully-fledged Fable.OrchardCore library to replicate all OrchardCore taghelpers and whatnot in F# frontend code (would take a huge amount of time, effort, and testing).
  2. create a Giraffe.Fluid port, find out how to generate Fluid TemplateContext instances from OC, find a liquid template inside a module (same as I was looking for .cshtml earlier), and render the output to a Giraffe handler.

Of the two, option 2 seems much more realistic, but I don't know if it'll even work. Need to know if F# projects approve .liquid files / whether I can get valid syntax highlighting and autocompletion (if there even is any?). And if I do go down that route, I also have to figure out how to find the files and load them in a module (same issue I had before).

In contrast, if I go with option 1, I'm guaranteed to "find my template" because the template code is directly written in F# code (and I can use all of the existing Fable libraries to help me), and I also get proper syntax highlighting, autocompletion, and other IDE features associated with Visual Studio (not to mention the language features of F# at my disposal).

Regardless, I've exhausted all of the time I can allocate to this task during work hours, so the remaining stuff will probably all need to be done in my off time. It's a shame that OC, and MVC in general, is so antagonistic to F# workflows, but I can't exactly blame it since the two just aren't inherently very agreeable.

I've already got a WIP Giraffe.Fluid library on my GitHub account so when I'm done with that, I'll try testing out option 2 in OC, but chances are, I won't be satisfied with that and will need to start option 1. But I'll keep updating this Issue as I move along, so the community is updated.

@Skrypt
Copy link
Contributor

Skrypt commented Sep 29, 2020

Option 2 will only give you the Fluid templates working without the TagHelpers I think. We do add TagHelpers in different modules of OC on top of the Fluid implementation. So we extend Fluid functionalities in some modules. But normally, if our modules that extend Fluid functionalities are loaded in your project the taghelpers should then work. Just like the Razor ones.

@willnationsdev
Copy link
Author

@Skrypt Well, for option 2, I'm just going to be adding Giraffe HttpHandlers that find a ".liquid" file template, load its text, and then use DI to find some sort of ITemplateContextFactory (or something similar) that can build me a TemplateContext from OC's modules, at which point, I can pass in my string, get the generated output, and pass that output to the handler for sending in the response. Just have to figure out the middle section to make sure I get all the right module support from OC when generating the output.

@sebastienros sebastienros added this to the backlog milestone Oct 1, 2020
@willnationsdev
Copy link
Author

willnationsdev commented Oct 31, 2020

Brief status update. I just recently managed to get a baseline Giraffe webserver finding a local .liquid file and rendering it with Fluid, so yay! Gonna refine the API more, but it's working!

Next steps...

  1. Refine API and publish it as an official Giraffe.Fluid nuget package in alpha.
  2. Figure out how to get the baseline TemplateContext from OrchardCore
  3. Figure out how to make an OrchardCore web app successfully find a .liquid file located in a module using the virtual file system, get the text from that file, and pass it to the Giraffe.Fluid code.
  4. Build on all of the above to create a fully working example of Giraffe code rendering a Fluid template from an OC module in an OC CMS web app.

Also, btw, I can't seem to find a decent Liquid syntax highlighter in Visual Studio so that sucks. May end up having to switch from VS to VS Code for development on my OC project, but I don't know how I'd use the whole IIS Express stuff from that (I'm guessing it's some dotnet command). Oh well. :-/ Looks like it just took a restart to load. Liquid template highlighting is working now~

Edit: I'm now back where I started with the app needing to find the Liquid file at runtime properly, so I'm going to try investigating the physical-path-during-development-and-virtual-path-in-production workflow in OrchardCore source code to see how that works. Thanks for all the help/explanation of that @jtkech. I'll post here again if I have further questions about it.

@willnationsdev
Copy link
Author

willnationsdev commented Nov 2, 2020

@Skrypt

So, looking at the OrchardCore source code, it seems as though the LiquidViewsFeatureProvider goes out, finds all .liquid files in each application module from the IFileProviders, and registers them as "LiquidPage" views in the MVC framework. That way, if someone tries to do View(), and it can find a matching .liquid file, it will be registered as a special type of RazorPage and will render away (the LiquidPage class has an override to delegate the rendering operation to the FluidTemplate from OrchardCore).

Rather than trying to manually fetch a FluidTemplateContext for the appropriate endpoint and dealing with physical paths vs virtual paths with embedded files, it seems like it would be simpler to just find a way to forcibly look up the MVC Razor view and let the ILiquidViewsFeatureProvider do the work of connecting my F# project's .liquid files with the requested view. So, I would actually still be using the Giraffe.Razor API, but I would adapt to not give a crap about the actual file path...somehow. I'll have to look more into how the actual MVC API works to see if I can adapt it. Like, if you just had a non-MVC project with no Controller, how would you take an incoming HttpRequest, generate a simple local Model, pass it to a requested View, get the generated output, and write that output to the HttpResponse? I think that's the kind of low-level operation I'm gonna have to figure out.

Edit: and I've already confirmed that my own F# project with a .liquid file in the Views directory does show up as an Asset in the Module for my Application, so it should get picked up by the LiquidViewsFeatureProvider which is looking in that Views folder to pull views.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants