Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcHostControl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information
namespace DotNetNuke.Web.Mvc
{
using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.UI;

using DotNetNuke.Services.Exceptions;
using DotNetNuke.UI.Modules;
using DotNetNuke.Web.Mvc.Routing;

public class AsyncMvcHostControl : MvcHostControl, IAsyncModuleControl
{
public AsyncMvcHostControl()
: base()
{
}

public AsyncMvcHostControl(string controlKey)
: base(controlKey)
{
}

protected override void OnInitInternal(EventArgs e)
{
if (this.ExecuteModuleImmediately)
{
this.Page.RegisterAsyncTask(new PageAsyncTask(this.ExecuteModuleAsync));
}
}

protected override void OnPreRenderInternal(EventArgs e)
{
this.Page.RegisterAsyncTask(new PageAsyncTask(this.OnPreRenderAsync));
}

protected async Task ExecuteModuleAsync(CancellationToken cancellationToken)
{
try
{
HttpContextBase httpContext = new HttpContextWrapper(HttpContext.Current);

var moduleExecutionEngine = GetModuleExecutionEngine();

this.Result = await moduleExecutionEngine.ExecuteModuleAsync(this.GetModuleRequestContext(httpContext), cancellationToken);

this.ModuleActions = this.LoadActions(this.Result);

httpContext.SetModuleRequestResult(this.Result);
}
catch (Exception exc)
{
Exceptions.ProcessModuleLoadException(this, exc);
}
}

private Task OnPreRenderAsync(CancellationToken cancellationToken)
{
try
{
if (this.Result == null)
{
return Task.CompletedTask;
}

var mvcString = RenderModule(this.Result);
if (!string.IsNullOrEmpty(Convert.ToString(mvcString, CultureInfo.InvariantCulture)))
{
this.Controls.Add(new LiteralControl(Convert.ToString(mvcString, CultureInfo.InvariantCulture)));
}
}
catch (Exception exc)
{
Exceptions.ProcessModuleLoadException(this, exc);
}

return Task.CompletedTask;
}
}
}
48 changes: 48 additions & 0 deletions DNN Platform/DotNetNuke.Web.Mvc/AsyncMvcSettingsControl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information

namespace DotNetNuke.Web.Mvc
{
using System;
using System.Threading;
using System.Threading.Tasks;

using DotNetNuke.Entities.Modules;
using DotNetNuke.UI.Modules;

public class AsyncMvcSettingsControl : AsyncMvcHostControl, IAsyncSettingsControl
{
public AsyncMvcSettingsControl()
: base("Settings")
{
this.ExecuteModuleImmediately = false;
}

/// <inheritdoc/>
public void LoadSettings()
{
throw new NotSupportedException("Async controls need to call LoadSettingsAsync.");
}

/// <inheritdoc/>
public Task LoadSettingsAsync(CancellationToken cancellationToken)
{
return this.ExecuteModuleAsync(cancellationToken);
}

/// <inheritdoc/>
public void UpdateSettings()
{
throw new NotSupportedException("Async controls need to call UpdateSettingsAsync.");
}

/// <inheritdoc/>
public async Task UpdateSettingsAsync(CancellationToken cancellationToken)
{
await this.ExecuteModuleAsync(cancellationToken);

ModuleController.Instance.UpdateModule(this.ModuleContext.Configuration);
}
}
}
26 changes: 9 additions & 17 deletions DNN Platform/DotNetNuke.Web.Mvc/DnnMvcHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
namespace DotNetNuke.Web.Mvc
{
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
Expand All @@ -26,7 +28,7 @@ namespace DotNetNuke.Web.Mvc

using Microsoft.Extensions.DependencyInjection;

public class DnnMvcHandler : IHttpHandler, IRequiresSessionState
public class DnnMvcHandler : HttpTaskAsyncHandler, IRequiresSessionState
{
public static readonly string MvcVersionHeaderName = "X-AspNetMvc-Version";

Expand All @@ -41,19 +43,13 @@ public DnnMvcHandler(RequestContext requestContext)

public RequestContext RequestContext { get; private set; }

/// <inheritdoc />
bool IHttpHandler.IsReusable => this.IsReusable;

internal ControllerBuilder ControllerBuilder
{
get => this.controllerBuilder ??= ControllerBuilder.Current;
set => this.controllerBuilder = value;
}

protected virtual bool IsReusable => false;

/// <inheritdoc />
void IHttpHandler.ProcessRequest(HttpContext httpContext)
public override async Task ProcessRequestAsync(HttpContext context)
{
SetThreadCulture();
MembershipModule.AuthenticateRequest(
Expand All @@ -65,18 +61,20 @@ void IHttpHandler.ProcessRequest(HttpContext httpContext)
Globals.GetCurrentServiceProvider().GetRequiredService<IHostSettings>(),
this.RequestContext.HttpContext,
allowUnknownExtensions: true);
this.ProcessRequest(httpContext);

var httpContextBase = new HttpContextWrapper(context);
await this.ProcessRequestAsync(httpContextBase, httpContextBase.Response.ClientDisconnectedToken);
}

protected internal virtual void ProcessRequest(HttpContextBase httpContext)
protected internal virtual async Task ProcessRequestAsync(HttpContextBase httpContext, CancellationToken cancellationToken)
{
try
{
var moduleExecutionEngine = GetModuleExecutionEngine();

// Check if the controller supports IDnnController
var moduleResult =
moduleExecutionEngine.ExecuteModule(this.GetModuleRequestContext(httpContext));
await moduleExecutionEngine.ExecuteModuleAsync(this.GetModuleRequestContext(httpContext), cancellationToken);
httpContext.SetModuleRequestResult(moduleResult);
this.RenderModule(moduleResult);
}
Expand All @@ -85,12 +83,6 @@ protected internal virtual void ProcessRequest(HttpContextBase httpContext)
}
}

protected virtual void ProcessRequest(HttpContext httpContext)
{
HttpContextBase httpContextBase = new HttpContextWrapper(httpContext);
this.ProcessRequest(httpContextBase);
}

private static void SetThreadCulture()
{
var portalSettings = PortalController.Instance.GetCurrentSettings();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace DotNetNuke.Web.Mvc.Framework.ActionFilters
using System;
using System.Globalization;
using System.Reflection;
using System.Threading.Tasks;
using System.Web.Mvc;

using DotNetNuke.Entities.Modules.Actions;
Expand Down Expand Up @@ -55,12 +56,20 @@ public override void OnActionExecuting(ActionExecutingContext filterContext)
methodName = this.MethodName;
}

var method = GetMethod(type, methodName);
var method = GetMethod(type, methodName, controller.IsAsync);

controller.ModuleActions = method.Invoke(instance, null) as ModuleActionCollection;
var result = method.Invoke(instance, null);
if (result is ModuleActionCollection moduleActions)
{
controller.ModuleActions = moduleActions;
}
else if (result is Task<ModuleActionCollection> taskResult)
{
controller.ModuleActionsAsync = taskResult;
}
}

private static MethodInfo GetMethod(Type type, string methodName)
private static MethodInfo GetMethod(Type type, string methodName, bool supportsAsync)
{
var method = type.GetMethod(methodName);

Expand All @@ -69,13 +78,13 @@ private static MethodInfo GetMethod(Type type, string methodName)
throw new NotImplementedException($"The expected method to get the module actions cannot be found. Type: {type.FullName}, Method: {methodName}");
}

var returnType = method.ReturnType.FullName;
if (returnType != "DotNetNuke.Entities.Modules.Actions.ModuleActionCollection")
var returnType = method.ReturnType;
if (returnType == typeof(ModuleActionCollection) || (supportsAsync && returnType == typeof(Task<ModuleActionCollection>)))
{
throw new InvalidOperationException("The method must return an object of type ModuleActionCollection");
return method;
}

return method;
throw new InvalidOperationException("The method must return an object of type ModuleActionCollection");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public void ExecuteResult(ControllerContext context, TextWriter writer)

if (this.View == null)
{
result = this.ViewEngineCollection.FindPartialView(context, this.ViewName);
result = this.FindView(context);
Comment thread
mitchelsellers marked this conversation as resolved.
this.View = result.View;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void ExecuteResult(ControllerContext context, TextWriter writer)

if (this.View == null)
{
result = this.ViewEngineCollection.FindView(context, this.ViewName, this.MasterName);
result = this.FindView(context);
this.View = result.View;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace DotNetNuke.Web.Mvc.Framework.Controllers
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Threading.Tasks;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.UI;
Expand Down Expand Up @@ -72,6 +73,12 @@ public ActionResult ResultOfLastExecute
/// <inheritdoc />
public ModuleActionCollection ModuleActions { get; set; }

/// <inheritdoc/>
public Task<ModuleActionCollection> ModuleActionsAsync { get; set; }

/// <inheritdoc/>
public bool IsAsync { get; set; }

/// <inheritdoc />
public ModuleInstanceContext ModuleContext { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace DotNetNuke.Web.Mvc.Framework.Controllers
{
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using System.Web.Mvc;
using System.Web.UI;

Expand All @@ -24,6 +25,10 @@ public interface IDnnController : IController

ModuleActionCollection ModuleActions { get; set; }

Task<ModuleActionCollection> ModuleActionsAsync { get; set; }

bool IsAsync { get; set; }

Comment on lines +28 to +31
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dnnsoftware/approvers do we have concerns about 3rd parties implementing this interface and being broken by this change?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, as this will result in a compilation error for any upstream user of IDnnController correct since they don't implement the interface members

Copy link
Copy Markdown
Contributor Author

@dimarobert dimarobert Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new IAsyncDnnController can be declared that has these properties and DnnController would implement both. Then a cast check would have to be made when they are accessed in the code, with an eventual fallback (probably throw?) if the controller does not have it but wants to run through the async code path.

One difficult situation with this might be the DnnMvcHandler route (rendering an MVC view through a direct request to it to do a partial render of the page), as there is no easy way there to figure out if the sync or async code path needs to be taken as that is not relying on a ModuleControl being present for the view to render, so that the ControlSrc format could tell us what to do, it simply matches on the ModuleInfo, controllerName, and actionName pair from the request path / query. So to figure out if DnnMvcHandler needs to end up calling ModuleApplication.ExecuteRequest() or ModuleApplication.ExecuteRequestAsync(), it would need to get a hold of the controller type first based on the pair above (which the inner ModuleApplication.ExecuteRequest{Async} already does) and then use reflection on the controller action to see if it returns Task. So we would be running the same logic twice.

ModuleInstanceContext ModuleContext { get; set; }

bool ValidateRequest { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
namespace DotNetNuke.Web.Mvc.Framework.Modules
{
using System.IO;
using System.Threading;
using System.Threading.Tasks;

public interface IModuleExecutionEngine
{
ModuleRequestResult ExecuteModule(ModuleRequestContext moduleRequestContext);

Task<ModuleRequestResult> ExecuteModuleAsync(ModuleRequestContext moduleRequestContext, CancellationToken cancellationToken);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question about modifying interface

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, we could have a separate IAsyncModuleExecutionEngine with the current ModuleExecutionEngine implementing both.


void ExecuteModuleResult(ModuleRequestResult moduleResult, TextWriter writer);
}
}
Loading
Loading