dnnsoftware / Dnn.Platform

DNN (formerly DotNetNuke) is the leading open source web content management platform (CMS) in the Microsoft ecosystem.
https://dnncommunity.org/
MIT License
1.02k stars 746 forks source link

Add Razor Pages Module Pipeline #2599

Open SkyeHoefling opened 5 years ago

SkyeHoefling commented 5 years ago

Living Document

This document is a living document and will be edited as we get feedback from community members to refine the Spec

Description of problem

There is no easy way to migrate DNN to .NET Core as Web.Forms does not exist in .NET Core. Adding support for a Razor Pages Module will support this soft migration path.

Description of solution

Add a new Module Pipeline that will be a carbon copy of .NET Core Razor Pages into the DNN Platform. The goal of this new module pipeline will to be use the EXACT same .zip installer and module code for both DNN on .NET Framework and DNN on .NET Core which will allow module developers to migrate their modules once to Razor Pages and they should work on their .NET version of DNN.

Please read the spec below for implementation details and the plan of action (which we can discuss in the comments)

The Spec

DNN Razor Pages will follow as close as we possibly can to feature parity with .NET Core Razor Pages. If you are unfamiliar with how it works please take a look at the docs from Microsoft. Every item in the Spec is a design element that is pulled from the Razor Pages implementation in .NET Core and our Proof of Concept of DNN running in .NET Core

DNN Manifest File

A minor update to the Module Loading pipeline will be needed in the ModuleControlFactory to allow a new type of file .razorpages.

Snippet Example Manifest:

<moduleControl>
    <controlKey />
    <controlSrc>DNNTAG.RazorPagesModule/Index.cshtml</controlSrc>
    <supportsPartialRendering>False</supportsPartialRendering>
    <controlTitle>RazorPagesModule</controlTitle>
    <controlType>View</controlType>
    <iconFile />
    <helpUrl />
    <viewOrder>0</viewOrder>
</moduleControl>

Module Control Properties

Property Description
controlKey
controlSrc The path to the root index.cshtml starting from the DesktopModules Folder
supportsPartialRendering
controlTitle
controlType
iconFile
helpUrl
viewOrder

note: This isn't the final spec as this is the module manifest from the existing mvc/razor pages implementation which is subject to change.

The View aka Razor Page

The view is a .cshtml Razor Page. The exact same razor syntax that you see in Razor Modules or MVC Modules. Razor Syntax (the code of a .cshtml page) is a combination of C# and HTML.

Simple View

@page
@model HelloWorld

<h1>@Model.Title</h1>
<div>@Model.Content</div>

Directives The top of the Razor Page has several directives that are required for the page to work correctly in DNN Razor Pages

Directive Description MVP Plan
@page Required at the top of every Razor Page to tell the runtime that the page is using Razor Pages instead of MVC or other framework Must Have
@using Includes a .NET namespace to be using in the C# code included in the Razor Page Must Have
@model Binds the specific Model to the view which allows the use of @Model syntax to retrieve values of the model's properties Must Have
@inject Injects different services or objects that are registered with the Razor Pages Dependency Injection Provider Nice to Have

View Context Utilities The DNN Platform has special utility libraries that are used throughout the different module platforms to make DNN specific API calls easier for the module developer.

API Utility Description
DnnUrlHelper Manages DNN specific URLs and routing
TBD We need feedback on required view context APIs

The Page Model

The Page Model is NOT a code behind

Every *.cshtml file in Razor Pages modules will have a corresponding *.cshtml.cs file which will inherit a DnnPageModel the base class will provide access to critical DNN, .NET Framework and .NET Core APIs depending on the runtime environment.

The Page Model simplifies binding a model to a view similar to how you would do it in MVC but with less convention based programming. The Page Model can have any number of properties on it that can be bound in the View using Razor Syntax. The properties can be assigned at Page Model Construction time or using different HTTP Handlers that are implemented in the Razor Pages spec.

Handlers Razor Pages requires several different HTTP Handles that execute depending on the type of HTTP Method that is invoked by the client. This allows the Page Model to execute different code depending on the type of action the user is trying to accomplish. For example if the user is just trying to retrieve data the module may have a HTTP GET Method implemented on their Page Model

Handler Route Example Description MVP Plan
GET https://localhot.dnndev.me/MyModulePage/ModuleId/1234/index The basic GET HTTP Request Handler to populate the model at request time Must Have
PUT https://localhot.dnndev.me/MyModulePage/ModuleId/1234/index TBD Won't Have
POST https://localhot.dnndev.me/MyModulePage/ModuleId/1234/index The POST HTTP Request Handler to submit data to the Page Model using [BindableProperty] Must Have
DELETE https://localhot.dnndev.me/MyModulePage/ModuleId/1234/index TBD Won't Have

Routing Table A Razor Pages module can have many pages which have many routing features. The table below documents how the folder structure in a Razor Pages Module will map to the DNN Routes

File Path Matching URL
/Pages/Index.cshtml https://localhost.dnndev.me/MyModulePage/ModuleId/1234/ or https://localhost/dnndev.me/MyModulePage/ModuleId/1234/Index
/Pages/Contact.cshtml https://localhost.dnndev.me/MyModulePage/ModuleId/1234/Contact
/Pages/Store/Contact.cshtml https://localhost.dnndev.me/MyModulePage/ModuleId/1234/Store/Contact
/Pages/Store/Index.cshtml https://localhost.dnndev.me/MyModulePage/ModuleId/1234/Store or https://localhost.dnndev.me/MyModulePage/ModuleId/1234/Store/Index

HTTP GET Page Model

public class HelloWorldModel : DnnPageModel
{
    public string Message { get; private set; } = "PageModel in C#";

    public IDnnActionResult OnGet()
    {
        Message += $" Server time is { DateTime.Now }";
    }
}

Async Handler Methods HTTP GET Page Model

public class HelloWorldModel : DnnPageModel
{
    public string Message { get; private set; } = "PageModel in C#";

    public async Task<IDnnActionResult> OnGetAsync()
    {
        // Simulate Service call
        await Task.Delay(100);

        Message += $" Server time is { DateTime.Now }";
    }
}

HTTP Post Page Model

public class HelloWorldModel : DnnPageModel
{
    [DnnBindProperty]
    public Customer Customer { get; set; }

    public async Task<IDnnActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Simulate a database save
        await Task.Delay(100);

        return RedirectToPage("/Index");
    }
}

DNN Base Classes

DnnPageModel The DnnPageModel provides a shared implementation of the .NET Core PageModel that will work in .NET Framework and .NET Core. This is the expected Model that the DNN Module Pipeline expects when the module is loading. If this file doesn't inherit from this class the Module will not load correctly. The DnnPageModel includes DNN specific APIs for URLs, HTTP and DNN.

An example of what the abstract base class may look like

public abstract class DnnPageModel
{
    protected DnnHelper Dnn { get; private set; }
    protected HtmlHelper Html { get; private set; }
    protected UrlHelper Url { get; private set; }
}

To get an idea on what the cross-platform DnnPageModel may look at take a look at the Proof of Concept I wrote last year. DnnPageModel. Be advised that this Proof of Concept did not implement the Razor Pages spec correctly so the code may not 100% be copied into the final solution.

Required Methods/APIs

Method/Property Name Status Description
ViewData Must Have
PageContext Must Have
HttpContext Must Have
Request Must Have
Url Must Have
RouteData Must Have
ModelState Must Have Handles model validation
User Must Have
Response Must Have
TempData Must Have
BadRequest() Must Have
Challenge() Must Have
Content() Must Have
File() Must Have
Forbid() Must Have
LocalRedirect()
LocalRedirectPermanent()
LocalRedirectPermanentPreserveMethod()
LocalRedirectPreserveMethod()
NotFound() Must Have
OnPageHandler() Must Have
Page() Must Have Returns the default OnGet() HTTP Handler
PhysicalFile()
RedirectPermanent()
RedirectPermanentPreserveMethod()
RedirectPreserveMethod()
RedirectToAction() Must Have
RedirectToActionPermanent()
RedirectToActionPermanentPreserveMethod()
RedirectToActionPreserveMethod()
RedirectToPage() Must Have
RedirectToPagePermanent()
RedirectToPagePermanentPreserveMethod()
RedirectToPagePreserveMethod()
RedirectToRoute()
RedirectToRoutePermanent()
RedirectToRoutePermanentPreserveMethod()
RedirectToRoutePreserveMethod()
SignIn() Research
SignOut() Research
StatusCode()
TryValidateModel()
Unauthorized() Must Have
Redirect() Must Have
TryUpdateModelAsync()

DnnBindProperty (Attribute) Provides the DNN Wrapper for binding properties to be used on POST HTTP requests. This needs to be included as a wrapper as the behavior doesn't exist in .NET Framework.

IDnnActionResult This is kind of an unknown at this point as this has been used with different names and purposes throughout the MVC pattern and the Proof of Concept.

This is a placeholder that needs to be flushed out more and how it will work between .NET Framework and .NET Core.

Dependency Injection

This is a placeholder and needs to be flushed out. It will not be included in the 9.4 release

Razor Modules vs. Razor Pages Modules

With the addition of a new Module Pipeline we will now have 2 module types that have very similar names and it will be our job as Open Source Maintainers to document this as best as we can. I am open to ideas on reducing the confusion here but I don't see any way around calling this new module pattern Razor Pages Modules as this is the name Microsoft uses.

Razor Modules are an existing Module Pipeline that uses a simple .cshtml or .vbhtml view and optionally a model binding. See "Why can't we use Razor Modules?" below for more info on why we can't use it

If you want more information on the existing Razor Module Platform take a look at the DotNetNuke.Web.Razor library in the Platform Code

Description of alternatives considered

This was originally an effort that I undertook in March of 2018 and was put on hold for several reasons. By taking our time considering how the implementation works we have learned quite a bit. The initial implementation was written as a fork of the MVC Module Platform which I don't think is appropriate anymore.

The new Razor Pages Module Platform will be a combination of the Razor Module Platform and the MVC Module Platform as they both have things that will be needed to accomplish this. The code we originally wrote in 2018 will still be useful, but some things will not be.

Take a look at what we did last year:

Again, some of this code will be re-used and some of it will be scrapped as we work towards a lean Razor Pages implementation in DNN.

Why can't we use Razor Modules?

I am glad you asked why can't we just use the existing Razor Modules that already exist in DNN today. This is a great question and I asked myself this as I was researching the DNN Module Pipeline.

Razor Modules are tightly coupled to several System.Web objects and will not work in a .NET Core implementation. Since there are many modules in the field that use Razor Modules I believe it is best to not create any breaking change for them.

The Razor Module pattern will be leveraged as it has the building blocks for completing a Razor Pages Module Pipeline

Migration Plan

Razor Modules

2613 Deprecates the existing Razor Module pattern. As we move forward with the spec and the implementation, we will document here how to migrate from an existing Razor Module to Razor Pages.

Screenshots

Not Applicable

Additional context

Add any other context about the feature that may be helpful with implementation.

Affected version

Affected browser

Implementation Plan

The plan is to create the Razor Pages Module Platform as an Experimental Feature Toggle for DNN 9.4 and have it turned off by default. This will allow developers to start testing it out and provide feedback as soon as possible.

<appSettings>
  <add key="Experimental:RazorPages" value="false" />
</appSettings>

When you turn on Razor Pages it will disable existing razor modules since the ControlSrc conflict

When we have determined Razor Pages is production ready we will remove the feature toggle

Linked Work Items

This new feature may be too large to complete in one Pull Request and we should break it down to Linked Work Items to make it more manageable. As we create the linked work items we should add to the table below

Work Item # Target Release Completed
#2613 9.3.1 PR Submitted #2618
#2615 9.4.0 PR Submitted #2644
#2619
#2637 9.3.1 PR Submitted #2638
mitchelsellers commented 5 years ago

This looks awesome. Two primary questions.

1.) For the page base, is there anything in .NET Framework that we should follow? Ideally, I'd love to not have anything too DNN specific in favor of injecting like what is done with .NET Core as a standard. (Think SignInManager, IHttpContextAccessor, etc.)

2.) Lets be sure when we implement to find a way to keep the files as .cshtml files, for VS/VS Code editor support etc.

SkyeHoefling commented 5 years ago

@mitchelsellers thanks for the feedback, to answer your questions:

  1. Take a look at POC PageModel as we used preprocessor directives to use different code for .NET Core vs .NET Framework. In the case of the PageModel I don't see an easy way around it and think we should have our own base class. As far as the APIs we can utilize a form of Dependency Injection so they can be included. If you look at the Directives table the plan as of right now is to include @inject for the .cshtml files.

    1. There are going to be some files that require a shared Dnn Base Class, but our goal is to try and make this as lean as possible.

    2. To further illustrate my point for needing a DnnPageModel take a look at the PageModel class that exists in .NET Core. Just about every property and method that exists there need to be replicated into our DnnPageModel. Anything that goes outside the scope of the PageModel should be injected and that I 100% agree.

    3. I don't think we are going to be able to identify every class that needs a base class right now, I wrote this up with pretty much a base class for everything to cover our basis as we develop this and realize certain things will need a base class.

  2. Making sure the files support VS/VS Code editor is at the top of the list.

Shared Services

Since we want the developer to be able to inject certain services do we have a small list of items we want injected?

david-poindexter commented 5 years ago

Fantastic work on this @ahoefling - exciting times!

bdukes commented 5 years ago

I would love it if the controlSrc could just point to the cshtml file. I had never heard of the Razor Module Pipeline before this RFC, and a quick GitHub search indicates it's not in use in any OSS module. Do you know of specific modules using the Razor Module Pipeline?

Regarding the PageModel base class, it appears that all of its properties are either coming from PageContext, which we could inherit from, or are coming via dependency injection, so we could override any behaviors that way. It would be amazing if it was possible to avoid having our own wrappers of these base types.

Thanks for your work on all of this @ahoefling, looking forward to seeing it all ironed out!

bdukes commented 5 years ago

Also, there's one recurring thought I've been having that I'll just lay out here in case it piques anyone's interest. Something that Andrew's demo last year highlighted was that, while you can target .NET Standard, you still need to have separate packages for .NET Framework and .NET Core, b/c the dependencies won't end up being the same. I'm wondering if we could create a new packaging system that used NuGet, which would allow a module package to be just a NuGet package, and then DNN would be able to unpack it and grab the right versions of assemblies. This would also give us :one: dependency management (e.g. my module depends on this other module, or some library, etc.) and :two: a simple means of hosting a collection of extensions that are available to install based on those dependencies (i.e. we can just stand up a NuGet server, and easily configure the site to be able to look at extra third party servers, or the main NuGet feed for libraries, etc.).

If there's interest in that idea, I suppose it should be spun off into its own RFC. @ahoefling, from your perspective, does that sound like it would make packaging/distributing/deploying simpler for Razor Pages Modules?

mitchelsellers commented 5 years ago

@bdukes If I could love this more times I would....a module as a NuGet package would be epic in my opinion

SkyeHoefling commented 5 years ago

@bdukes

I would love it if the controlSrc could just point to the cshtml file.

I spoke with @mitchelsellers about this and we agree it will be best to deprecate Razor Modules and I just created #2613 which will deprecate it. This means we can update the module definition to use .cshtml instead of .razorpages. What we can do is if someone turns the feature toggle on it will disable Razor Module and turn on Razor Pages Modules. I will update the spec to reflect this change.

@bdukes

It would be amazing if it was possible to avoid having our own wrappers of these base types.

I 100% agree with this statement and there are going to be cases where we can't get away from wrappers such as the DnnPageModel. I wrote the original spec to wrap everything originally because at this point in time we don't know what need wrappers and what doesn't. As we create issues that implement those features we can determine what is needed and update the spec here.

@bdukes

If there's interest in that idea, I suppose it should be spun off into its own RFC

I 100% support this idea and think you should create an RFC to further investigate it

@bdukes

@ahoefling, from your perspective, does that sound like it would make packaging/distributing/deploying simpler for Razor Pages Modules?

Creating a NuGet package to use for our extension installer won't really help or hurt Razor Pages Modules. While we need to define the installation process more in the spec the working idea I have doesn't require .NET Framework or .NET Core assemblies in the module. The module will reference 100% .NET Standard assemblies and when the module is installed into DNN it will have the expectation that the .NET Framework or .NET Core assemblies will already be there.

stephen-lim commented 5 years ago

Hi @ahoefling @bdukes

DotNetNuke.Web.Razor was heavily promoted as the new way to develop SPA modules. See blog post. I know many other developers built all kinds of things around it.

We use rely on it heavily to render Razor in a SPA setup for a very large module with hundreds of thousands of lines of code. It's extremely useful because it allowed us to gradually migrate one page at a time mixed in from legacy Web forms without rewriting a new module. I'm concerned the removal of it will break badly without leaving sufficient time to migrate to Razor Pages.

Can you please suggest a solution?

mitchelsellers commented 5 years ago

@stephen-lim Just to make sure that I understand, you are using the API's in DNN to take a .cshtml file and give it a model to render to HTML?

If so, this is something that you can, and should, use the pure Razor libraries for. I know that @mikesmeltzer was talking about a potential blog post if this is the case to outline how to accomplish this.

stephen-lim commented 5 years ago

@mitchelsellers Yes, but it's a lot more than just a RazorEngine type of thing. We need to use many helpers in the Razor library that make this step usable in SPA:

DotNetNuke.Web.Razor.Helpers.DnnHelper DotNetNuke.Web.Razor.Helpers.HtmlHelper DotNetNuke.Web.Razor.Helpers.UrlHelper

SkyeHoefling commented 5 years ago

@stephen-lim this is a big concern and I don't want to place any problems on your team or organization as a migration can be quite time-consuming.

@stephen-lim

Can you please suggest a solution?

We already have a migration plan that is pretty lightweight and you can implement it today even if you aren't on the latest version. I just submitted a similar solution to #2638 which is my official recommendation for a migration plan.

private StringWriter RenderTemplate(string virtualPath, dynamic model)
{
    var page = WebPageBase.CreateInstanceFromVirtualPath(virtualPath);
    var httpContext = new HttpContextWrapper(HttpContext.Current);
    var pageContext = new WebPageContext(httpContext, page, model);

    var writer = new StringWriter();

    if (page is WebPage)
    {
        page.ExecutePageHierarchy(pageContext, writer);
    }
    else
    {
        var razorEngine = new RazorEngine(virtualPath, null, null);
        razorEngine.Render<dynamic>(writer, model);
    }

    return writer;
}
stephen-lim commented 5 years ago

@ahoefling In your example, how would the Razor code access the DNN Helpers to provide context for page building? This is a key requirement

SkyeHoefling commented 5 years ago

@stephen-lim

We need to use many helpers in the Razor library that make this step usable in SPA:

DotNetNuke.Web.Razor.Helpers.DnnHelper DotNetNuke.Web.Razor.Helpers.HtmlHelper DotNetNuke.Web.Razor.Helpers.UrlHelper

This is great insight as to things you need.

@mitchelsellers the APIs referenced are duplicated throughout the different module pipelines. Outside of the UrlHelper (which needs to be specific between the pipelines) we should be able to abstract those out so each module pipeline uses a shared instance. I'm not trying to add more work for us, but it could be worth the investigation.

Another thing to consider with the deprecation of the Razor library we plan to support very similar functionality using Razor Pages which will be built on top of the same fundamental technology. As we flush out the API we will make sure to have these APIs included. We are hoping to get some experimental features available soon in 9.4.0 where we will have a very good idea on the migration plan.

We are trying to make this migration from Razor Engine to Razor Pages to be as simple and painless as possible.

@stephen-lim when we get the experimental feature added to 9.4 could you test a migration for us to see if it covers your needs?

stephen-lim commented 5 years ago

@ahoefling Yes, i'll be happy to test.

mitchelsellers commented 5 years ago

@ahoefling 100%, the conversation thus far is that we will need to look at new ways to get access to the same information.

Looking at this quickly

We have a lot of duplication, if we plan to get to .NET Core, it will be sad, but a reality that to have a quality product that is easy to understand we will need to get rid of some duplication and "less than perfect' implementations. The goal here is to make notes EARLY to try and identify usage, as we don't have any method to actually report on said usage.

SkyeHoefling commented 5 years ago

@mitchelsellers I love your idea hear of moving toward Dependency Injection for these APIs. While related to this work I don't think it needs to be part of the spec. Once we get the .NET 4.7.2 we need to start building a spec on Dependency Injection on how we see it being implemented.

@stephen-lim once we figure out the Dependency Injection story we can flush out how it will work with Razor Pages. Here is how I see it working without looking at the API to give everyone a quick idea

@injects DnnUrlHelp
@injects HtmlHelper
@injects DnnHelper

@* the rest of your cshtml markup goes here *@

Using the @injects directive in a .cshtml is a standard built into the razor spec from microsoft which will be supported in .NET Core

stephen-lim commented 5 years ago

@ahoefling isn't RazorEngine under DotNetNuke.Web.Razor namespace that is being deprecated?

var razorEngine = new RazorEngine(virtualPath, null, null);

stephen-lim commented 5 years ago

@ahoefling In your spec above, it would be nice if the routing can be dynamically changed such that we can return Razor pages from different folder paths than set in convention (even if they have to be inside the "pages" folder) and/or support partial pages. I recall the lack of support for partial views in the initial DNN MVC implementation disappointed a lot of folks when it was announced.

SkyeHoefling commented 5 years ago

@stephen-lim

it would be nice if the routing can be dynamically changed such that we can return Razor pages from different folder paths than set in convention

There are 2 parts to this question that I want to address.

Routing Table

The routing table mentioned in the spec (copied here for your convenience) is a carbon copy of how routing works in Razor Pages. I don't know of a special route manager, it is pretty simple the route is based on where the page is located in the file system.

File Path Matching URL
/Pages/Index.cshtml https://localhost.dnndev.me/MyModulePage/ModuleId/1234/ or https://localhost/dnndev.me/MyModulePage/ModuleId/1234/Index
/Pages/Contact.cshtml https://localhost.dnndev.me/MyModulePage/ModuleId/1234/Contact
/Pages/Store/Contact.cshtml https://localhost.dnndev.me/MyModulePage/ModuleId/1234/Store/Contact
/Pages/Store/Index.cshtml https://localhost.dnndev.me/MyModulePage/ModuleId/1234/Store or https://localhost.dnndev.me/MyModulePage/ModuleId/1234/Store/Index

Return Different Views

This feature of returning a different view is something that is built into MVC and is not a feature of Razor Pages. However there is support for Redirect and RedirectToAction which will work just like they do in MVC. That means in a OnPost() method on a PageModel you can tell the server to redirect the user to a different Razor Page or a MVC Action.

stephen-lim commented 5 years ago

Hi @ahoefling, the RazorHost module is used in many places and I see it has a dependency on DotNetNuke.Web.Razor, will this continue to work after v11?

valadas commented 5 years ago

Hi Guys, I am trying to cleanup the backlog and assign each issue in the release we hope to resolve it (nothing set in stone, we can always change that assigned release). Should this be an item assigned to 10 or 11 or other ?

SkyeHoefling commented 5 years ago

@valadas this item should be assigned to v10. Once v9.4 reaches GA and is released I plan to start submitting PRs for the Razor Pages Implementation. The intention is to get Razor Pages into DNN for v10 in an experimental mode if it isn't ready for production. Everything is going to be behind a feature toggle so we can make that decision at the last minute.

@stephen-lim RazorHost uses the Razor Module pattern in DNN and it is marked for deprecation in v11 (#2613). However, we are going to try and identify some migration strategies so people will have a path forward.

SkyeHoefling commented 4 years ago

I have been thinking about how the manifest file is going to look as of right now we have agreed upon the current standard:

<moduleControl>
    <controlKey />
    <controlSrc>DNNTAG.RazorPagesModule/Index.cshtml</controlSrc>
    <supportsPartialRendering>False</supportsPartialRendering>
    <controlTitle>RazorPagesModule</controlTitle>
    <controlType>View</controlType>
    <iconFile />
    <helpUrl />
    <viewOrder>0</viewOrder>
</moduleControl>

The breaking change in the manifest with other module patterns is the controlSrc as the Razor 3 module pattern depends on the .cshtml extension. We have discussed adding a feature toggle to alleviate this problem, however this doesn't take into account AspNetCore MVC modules (when we get there).

I am proposing we change the spec to support the following convention:

<moduleType>RazorPages</moduleType>
<controlSrc>DNNTAG.RazorPagesModule/Index.cshtml</controlSrc>

OR

We can add a custom extension such as .razorpages.cshtml similar to how MVC modules work with the extension of .mvc. This isn't an idea I support as we have been discussing getting away from using the custom extension .mvc since it creates confusion to new users as it is a virtual path and doesn't map to any view files because they are .cshtml in MVC modules.

If we add some field to denote what type of module the manifest is defining this would allow MVC, Razor3 and Razor Pages to live side-by-side in 10.x which was not part of the original plan but would reduce any hard breaking changes in adopting Razor Pages.

I am not 100% sold on the name moduleType but would like to have something like that property as it makes it much easier to decide which module pipeline

cc: @mitchelsellers @bdukes and anyone else who wants to participate

mitchelsellers commented 4 years ago

Given that we allow users to "mix" module types, I think it would be good to support a <controlType> property If not set, it would fall back to the current discovery logic.

I would not want to use the name moduleType designation though as it is possible, and probable taht modules will need to use different types of controls.

bdukes commented 4 years ago

We're already using controlType to designate the SecurityAccessLevel. What would y'all think about adding an optional type attribute to the controlSrc element?

mitchelsellers commented 4 years ago

That would work.

SkyeHoefling commented 4 years ago

I am working through this in my branch for Razor Pages and it occurred to me I really don't understand the difference betweencontrolSrc and controlKey. They are used differently in different module patterns. What was the purpose and differences between these?

Maybe we could leverage the controlKey as a type identifier for AspNetCore style modules

bdukes commented 4 years ago

controlKey is the built-in mechanism for routing withing "legacy" module patterns. I guess it doesn't really apply in MVC because MVC brings its own routing.

When a URL contains ?ctl=Edit&mid=321, that instructs DNN to loads the control with key Edit for the module with ID 321. This results in "module isolation", meaning that only that module loads on the page, which results in UI quirkiness (e.g. your "display on all pages" modules go missing), so a lot of vendors avoid using it, handling the routing inside the "main view control".

I think that piggybacking on controlKey would be a breaking change.

valadas commented 4 years ago

I think a type attribute to the controlSrc is fine IMO.

SkyeHoefling commented 4 years ago

I like the idea of adding an optional type attribute to the controlSrc so the manifest record would look something like this:

<controlSrc type="RazorPages">path/to/index.cshtml</controlSrc>

I am going to experiment with this and see how it works before committing it to the spec.

Binding Redirects

Razor Pages brings in a lot of new assemblies to the bin directory 40+. In discussion around the Dependency Injection PR #3147 @bdukes mentioned that strongly-named assemblies should always have binding redirects. https://github.com/dnnsoftware/Dnn.Platform/pull/3147#issuecomment-542280903

Should we be adding a binding redirect for every last strongly-named assembly that is brought in? This could be very hard to maintain, but I think it may be necessary to secure the new module pipeline.

bdukes commented 4 years ago

Do we need to adjust how we're managing these assemblies? When installing assemblies with a package, the assembly installer automatically adds a binding redirect for strong-named assemblies. Is it possible to include these assemblies in a library package?

stephen-lim commented 4 years ago

Are the Razor pages *.cshtml files expected to reside in the \DesktopModules\MyModule\Pages folder or simply \DesktopModules\MyModule without the Pages folder?

valadas commented 4 years ago

Don't quote me on this, but I believe it can be one or the other as long as the path to it is declared in the manifest...

SkyeHoefling commented 4 years ago

@stephen-lim this is a great question, thanks for asking it.

Are the Razor pages *.cshtml files expected to reside in the \DesktopModules\MyModule\Pages folder or simply \DesktopModules\MyModule without the Pages folder?

The intent is to have a Pages folder as you described \DesktopModules\MyModule\Pages. This is subject to change as I am researching if we can have the Pages embedded into the assembly which is something that appears to be common with ASP.NET Core.

All of this is subject to change, but once I have this implemented a little further I'll bring back the details here so we can all be informed

stephen-lim commented 4 years ago

FYI. Razor Pages also have \Areas folder adjacent to the \Pages folder but is not widely used. If we drop the \Pages folder, we may make it harder to support other "special" folders introduced by ASP.NET in the future.

https://www.learnrazorpages.com/razor-pages/routing

SkyeHoefling commented 4 years ago

@stephen-lim

Razor Pages also have \Areas folder adjacent to the \Pages folder

Thanks for sharing the \Areas folders and how they are adjacent to \Pages. This was a missed feature when DNN implemented MVC which I wish was there and our goal with Razor Pages is 100% feature parity. We may not hit that goal, but we want to aim for 100%

I need to clarify something I said earlier

@ahoefling

I am researching if we can have the Pages embedded into the assembly

If at all possible there will be no .cshtml files in the \Pages directory and it will be stored in the assembly itself. You would store styles and scripts in the module folder but the code will be pulled out of the assembly when the route is requested. If you create a new Razor Pages application the output directory doesn't have any .cshtml files, the goal is to mimic this behavior.

I am still researching and don't have any more details than what I just posted. Once I have more to share I will comment here

stephen-lim commented 4 years ago

@ahoefling,

I hope your vision includes feedback from module developers. I'll speak a bit about my scenarios and hopefully it can give you some context for your vision. I think my situation is a little representative of what serious module developers need to deal with when migrating to a future Razor Pages.

Scenario 1. We develop a very large e-commerce module in Web forms with nearly a hundred .ascx files and not far from a million lines of code. Keeping in mind everyone has limited resources and time to re-architecture/re-write, therefore, Razor Pages presents a perfect opportunity to convert from .ascx to .cshtml (one for one). Our module also allows users to create custom templates using Razor syntax (.cshtml). Through the use of RazorEngine, it will render the template and output into a Web forms placeholder.

We need an implementation where we can translate one .ascx file to one .cshtml file.

We also need an implementation that can dynamically take any .cshtml file path and render it to string. The Razor Pages specs allow calling Html.Partial(path, model) inside another .cshtml file so I think this will work fine as long as we can include arbitrary .cshtml files created by users somewhere in the module folder.

Scenario 2. We have another Web forms scheduler module that executes bits of user-saved Razor code on a scheduled basis. This is done through the RazorEngine.

We need the ability to call a similar RazorEngine to execute arbitrary razor code in the background.

Scenario 3. We have customers that use the RazorHost module (this is core module in DNN) to render dynamic content on the site.

We need to make sure RazorHost module continues to be supported in a future Razor Pages world.

If you need more information, I'll be happy to schedule a screen share or send more information your way. Thank you.

mitchelsellers commented 4 years ago

@stephen-lim

The goal of changing support for development methodologies within the DNN Platform is to ensure that we have a robust platform that is following the current best practices, and supportive of developer best practices. Part of this, is continued discussions around the methods in which developers are leveraging the platform, additionally, it will be re-aligning this to better support the standards set by Microsoft etc as well as ensuring that we have the most secure platform possible.

Specifically to your points.

Scenario 1: This is something that should be supported but WILL change. You should NOT be using the DNN Razor Pages engine, but the actual standard Razor engine for custom processing.

Scenario 2: This also should be possible using the .NET Razor Engine

Scenario 3: The usage of the RazorHost module is something that still needs research, as there are many implications of this model that impact us. It would be helpful I believe for all of us to get better visibility into the usage of this feature, as it is very little used, but those that do it seem to have a solid dependency.

thabaum commented 4 years ago

This is the most awesome issue I ever read !! I am going to have to spend a few days catching up on all this kick ass stuff... wow!

stephen-lim commented 4 years ago

Razor Pages is very promising as it allows Web forms to be migrated over relatively easy. Take a look at this article. It talks about how each concept in Razor Pages compares to Web Forms.

Razor Pages: the Natural Successor to Web Forms

bogdan-litescu commented 4 years ago

This was a very interesting read.

Same as Stephen, we allow users to create templates in various modules. These templates are saved on disk or DB. Then, they are compiled at runtime and injected into a container, which most of the time is a Webforms module. For example, users create Razor tokens in My Tokens that they use in HTML modules.

We need a smooth way to transition so we don't end up again in the situation we had with Dependency Injection when we worked 6 months to be compatible again with newer versions of DNN.

stephen-lim commented 4 years ago

@mitchelsellers , in Scenario 2, we would still need a wrapper around the .NET RazorEngine to pass the module/portal context that feeds into the helper classes (e.g. Dnn.Portal.PortalId). I understand you are guys are wanting to drop the DotNetNuke.Web.Razor because of naming reasons. I think it's somewhat a weak argument considering how much breakage this is going to cause. Please consider renaming the namespace and if not, offer a wrapper for the .NET Razor Engine.

In Scenario 3, let's not forget Razor Host is a core feature and is easy to use, we can expect people to have created all kinds of small tasks that needed access to .NET code. I know of a few customer sites that did this. It would be a nice-to-have if you can offer an easy workaround for them.

mitchelsellers commented 4 years ago

Scenario 2: We don't support background processing as-is that is module aware today, and adding support for that is going to be outside the scope of what we can handle with reliability. (See similar concerns with the Scheduler etc.)

Scenario 3: I haven't been able to obtain any real validation of the scope of usage for this feature, we still need to try and find a way to quantify this. Possibly with marking as "to be removed" or otherwise to try and better gather adoption information.

DotNetNuke.Web.Razor needs to go away for more than just naming, it isn't a proper pattern that aligns with the long-term strategies of Microsoft. So sadly, there will be breaking changes.

But if using just a Razor engine for templating, the DNN engine still should NOT be the engine used. Sadly things were marked public that shouldn't but that was all before the time we had controls in place.

I should note, this work is temporarily on hold due to other issues however.

stephen-lim commented 4 years ago

@mitchelsellers Scenario 2 isn't about scheduling. We don't need that. It's about using Razor as templating. It works now because DNN RazorEngine provides the module/portal context that feeds into the Helper classes. I believe a similar wrapper is all that is needed if we use the .NET RazorEngine.

If you can provide a mechanism to render arbitrary Razor code (with DNN context) like said wrapper, perhaps 3rd party developers can easily create replacement module to render arbitrary .cshtml file just like it is being done today by RazorHost. This will allow a migration path for the affected sites.

mitchelsellers commented 4 years ago

Good to know

mikesmeltzer commented 4 years ago

@stephen-lim I've created a working unofficial workaround for this using the MVC pipeline to render any arbitrary .cshtml file instead of the DNN Razor Engine. Andrew and I had thrown around some ideas when he first created this feature / issue first as some of my extensions use the DNN Razor Engine too.

To @mitchelsellers point, we don't really know how many developers / solutions are piggy backing on the DNN Razor Engine and it isn't intended to be a supported public feature (even though there are people using it). Perhaps the MVC pipeline workaround could be "good enough" for those that run into challenges in the future with existing solutions leaving new users to take advantage of the officially supported methods going forward.

I'll try to get a blog post written about it sometime. Since this work isn't active right now we have some time.

stephen-lim commented 4 years ago

@mikesmeltzer I look forward to reading how this would work and test. Please share as soon as you can! :)

I argue the DNN Razor Engine API is well known by seasoned developers. DNN Razor was heavily publicized in the past around the time of SPA. Here is one popular blog on DNN site that teaches people create modules using DNN Razor. This is the first Google result when you search for "how to build dnn module using razor" and 4th result with "how to build dnn module using spa". Ironically, the 2nd result is about using RazorHost.

stephen-lim commented 3 years ago

Today, a customer is concerned DNN is no longer viable and secure after reading this article. I like to reassure him that we have a path for going forward. I'm hopeful because I believe if we can convert one Web forms page to one Razor Page, we have a good chance to migrate to .NET Core.

mitchelsellers commented 3 years ago

@stephen-lim I still think that adding support for Razor Pages, if we can, would be nice. However, there are technical limitations that will possibly limit this a bit.

We have discussed some of the technical future here: https://dnncommunity.org/blogs/Post/7625/The-Technical-Future-of-DNN

mainmind83 commented 1 month ago

so... is only migration to Oqtane possible?