diff --git a/src/Umbraco.Web.BackOffice/ActionResults/JsonNetResult.cs b/src/Umbraco.Web.BackOffice/ActionResults/JsonNetResult.cs
new file mode 100644
index 0000000000..24f4ec51b8
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/ActionResults/JsonNetResult.cs
@@ -0,0 +1,55 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using Newtonsoft.Json;
+
+namespace Umbraco.Web.BackOffice.ActionResults
+{
+ ///
+ /// Custom json result using newtonsoft json.net
+ ///
+ public class JsonNetResult : IActionResult
+ {
+ public Encoding ContentEncoding { get; set; }
+ public string ContentType { get; set; }
+ public object Data { get; set; }
+
+ public JsonSerializerSettings SerializerSettings { get; set; }
+ public Formatting Formatting { get; set; }
+
+ public JsonNetResult()
+ {
+ SerializerSettings = new JsonSerializerSettings();
+ }
+
+ public Task ExecuteResultAsync(ActionContext context)
+ {
+ if (context is null)
+ throw new ArgumentNullException(nameof(context));
+
+ var response = context.HttpContext.Response;
+
+ response.ContentType = string.IsNullOrEmpty(ContentType) == false
+ ? ContentType
+ : System.Net.Mime.MediaTypeNames.Application.Json;
+
+ if (!(ContentEncoding is null))
+ response.Headers.Add(Microsoft.Net.Http.Headers.HeaderNames.ContentEncoding, ContentEncoding.ToString());
+
+ if (!(Data is null))
+ {
+ using var bodyWriter = new StreamWriter(response.Body);
+ using var writer = new JsonTextWriter(bodyWriter) { Formatting = Formatting };
+
+ var serializer = JsonSerializer.Create(SerializerSettings);
+ serializer.Serialize(writer, Data);
+
+ writer.Flush();
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs
new file mode 100644
index 0000000000..dfc28f1412
--- /dev/null
+++ b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs
@@ -0,0 +1,174 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Routing;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.Extensions.DependencyInjection;
+using Umbraco.Composing;
+using Umbraco.Core.IO;
+using Umbraco.Core.Models.PublishedContent;
+using Umbraco.Web.Routing;
+
+namespace Umbraco.Web.Website.ActionResults
+{
+ public class RedirectToUmbracoPageResult : IActionResult
+ {
+ private IPublishedContent _publishedContent;
+ private readonly int _pageId;
+ private readonly NameValueCollection _queryStringValues;
+ private readonly IPublishedUrlProvider _publishedUrlProvider;
+ private readonly IUmbracoContextAccessor _umbracoContextAccessor;
+ private string _url;
+
+ private string Url
+ {
+ get
+ {
+ if (!string.IsNullOrWhiteSpace(_url)) return _url;
+
+ if (PublishedContent is null)
+ throw new InvalidOperationException($"Cannot redirect, no entity was found for id {PageId}");
+
+ var result = _publishedUrlProvider.GetUrl(PublishedContent.Id);
+
+ if (result == "#")
+ throw new InvalidOperationException(
+ $"Could not route to entity with id {PageId}, the NiceUrlProvider could not generate a URL");
+
+ _url = result;
+
+ return _url;
+ }
+ }
+
+ private int PageId => _pageId;
+
+ private IPublishedContent PublishedContent
+ {
+ get
+ {
+ if (!(_publishedContent is null)) return _publishedContent;
+
+ //need to get the URL for the page
+ _publishedContent = _umbracoContextAccessor.UmbracoContext.Content.GetById(_pageId);
+
+ return _publishedContent;
+ }
+ }
+
+ ///
+ /// Creates a new RedirectToUmbracoResult
+ ///
+ ///
+ ///
+ public RedirectToUmbracoPageResult(int pageId, IPublishedUrlProvider publishedUrlProvider)
+ {
+ _pageId = pageId;
+ _publishedUrlProvider = publishedUrlProvider;
+ }
+
+ ///
+ /// Creates a new RedirectToUmbracoResult
+ ///
+ ///
+ ///
+ ///
+ public RedirectToUmbracoPageResult(int pageId, NameValueCollection queryStringValues, IPublishedUrlProvider publishedUrlProvider)
+ {
+ _pageId = pageId;
+ _queryStringValues = queryStringValues;
+ _publishedUrlProvider = publishedUrlProvider;
+ }
+
+ ///
+ /// Creates a new RedirectToUmbracoResult
+ ///
+ ///
+ ///
+ ///
+ public RedirectToUmbracoPageResult(int pageId, string queryString, IPublishedUrlProvider publishedUrlProvider)
+ {
+ _pageId = pageId;
+ _queryStringValues = ParseQueryString(queryString);
+ _publishedUrlProvider = publishedUrlProvider;
+ }
+
+ ///
+ /// Creates a new RedirectToUmbracoResult
+ ///
+ ///
+ ///
+ ///
+ public RedirectToUmbracoPageResult(IPublishedContent publishedContent, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor)
+ {
+ _publishedContent = publishedContent;
+ _pageId = publishedContent.Id;
+ _publishedUrlProvider = publishedUrlProvider;
+ _umbracoContextAccessor = umbracoContextAccessor;
+ }
+
+ ///
+ /// Creates a new RedirectToUmbracoResult
+ ///
+ ///
+ ///
+ ///
+ ///
+ public RedirectToUmbracoPageResult(IPublishedContent publishedContent, NameValueCollection queryStringValues, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor)
+ {
+ _publishedContent = publishedContent;
+ _pageId = publishedContent.Id;
+ _queryStringValues = queryStringValues;
+ _publishedUrlProvider = publishedUrlProvider;
+ _umbracoContextAccessor = umbracoContextAccessor;
+ }
+
+ ///
+ /// Creates a new RedirectToUmbracoResult
+ ///
+ ///
+ ///
+ ///
+ ///
+ public RedirectToUmbracoPageResult(IPublishedContent publishedContent, string queryString, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor)
+ {
+ _publishedContent = publishedContent;
+ _pageId = publishedContent.Id;
+ _queryStringValues = ParseQueryString(queryString);
+ _publishedUrlProvider = publishedUrlProvider;
+ _umbracoContextAccessor = umbracoContextAccessor;
+ }
+
+ public Task ExecuteResultAsync(ActionContext context)
+ {
+ if (context is null) throw new ArgumentNullException(nameof(context));
+
+ var httpContext = context.HttpContext;
+ var ioHelper = httpContext.RequestServices.GetRequiredService();
+ var destinationUrl = ioHelper.ResolveUrl(Url);
+
+ if (!(_queryStringValues is null) && _queryStringValues.Count > 0)
+ {
+ destinationUrl += "?" + string.Join("&",
+ _queryStringValues.AllKeys.Select(x => x + "=" + HttpUtility.UrlEncode(_queryStringValues[x])));
+ }
+
+ var tempData = httpContext.RequestServices.GetRequiredService();
+ tempData?.Keep();
+
+ httpContext.Response.Redirect(destinationUrl);
+
+ return Task.CompletedTask;
+ }
+
+ private NameValueCollection ParseQueryString(string queryString)
+ {
+ return !string.IsNullOrEmpty(queryString) ? HttpUtility.ParseQueryString(queryString) : null;
+ }
+ }
+}
diff --git a/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoUrlResult.cs b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoUrlResult.cs
new file mode 100644
index 0000000000..929cdb85e6
--- /dev/null
+++ b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoUrlResult.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Umbraco.Web.Website.ActionResults
+{
+ ///
+ /// Redirects to the current URL rendering an Umbraco page including it's query strings
+ ///
+ ///
+ /// This is useful if you need to redirect
+ /// to the current page but the current page is actually a rewritten URL normally done with something like
+ /// Server.Transfer. It is also handy if you want to persist the query strings.
+ ///
+ public class RedirectToUmbracoUrlResult : IActionResult
+ {
+ private readonly IUmbracoContext _umbracoContext;
+
+ ///
+ /// Creates a new RedirectToUmbracoResult
+ ///
+ ///
+ public RedirectToUmbracoUrlResult(IUmbracoContext umbracoContext)
+ {
+ _umbracoContext = umbracoContext;
+ }
+
+ public Task ExecuteResultAsync(ActionContext context)
+ {
+ if (context is null) throw new ArgumentNullException(nameof(context));
+
+ var destinationUrl = _umbracoContext.OriginalRequestUrl.PathAndQuery;
+ var tempData = context.HttpContext.RequestServices.GetRequiredService();
+ tempData?.Keep();
+
+ context.HttpContext.Response.Redirect(destinationUrl);
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs b/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs
new file mode 100644
index 0000000000..5019dbd7c2
--- /dev/null
+++ b/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs
@@ -0,0 +1,168 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Controllers;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.AspNetCore.Mvc.ViewEngines;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using Umbraco.Core;
+using Umbraco.Core.Logging;
+
+namespace Umbraco.Web.Website.ActionResults
+{
+ ///
+ /// Used by posted forms to proxy the result to the page in which the current URL matches on
+ ///
+ public class UmbracoPageResult : IActionResult
+ {
+ private readonly IProfilingLogger _profilingLogger;
+
+ public UmbracoPageResult(IProfilingLogger profilingLogger)
+ {
+ _profilingLogger = profilingLogger;
+ }
+
+ public Task ExecuteResultAsync(ActionContext context)
+ {
+ var routeData = context.RouteData;
+
+ ResetRouteData(routeData);
+ ValidateRouteData(routeData);
+
+ var routeDef = (RouteDefinition)routeData.DataTokens[Constants.Web.UmbracoRouteDefinitionDataToken];
+ routeData.Values["action"] = routeDef.ActionName;
+
+ var factory = context.HttpContext.RequestServices.GetRequiredService();
+ Controller controller = null;
+
+ if (!(context is ControllerContext controllerContext))
+ return Task.FromCanceled(new System.Threading.CancellationToken());
+
+ try
+ {
+ controller = CreateController(controllerContext, factory, routeDef);
+
+ CopyControllerData(controllerContext, controller);
+
+ ExecuteControllerAction(controllerContext, controller);
+ }
+ finally
+ {
+ CleanupController(controllerContext, controller, factory);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Executes the controller action
+ ///
+ private void ExecuteControllerAction(ControllerContext context, Controller controller)
+ {
+ using (_profilingLogger.TraceDuration("Executing Umbraco RouteDefinition controller", "Finished"))
+ {
+ var aec = new ActionExecutingContext(context, new List(), new Dictionary(), controller);
+ var actionExecutedDelegate = CreateActionExecutedDelegate(aec);
+
+ controller.OnActionExecutionAsync(aec, actionExecutedDelegate);
+ }
+ }
+
+ ///
+ /// Creates action execution delegate from ActionExecutingContext
+ ///
+ private static ActionExecutionDelegate CreateActionExecutedDelegate(ActionExecutingContext context)
+ {
+ var actionExecutedContext = new ActionExecutedContext(context, context.Filters, context.Controller)
+ {
+ Result = context.Result,
+ };
+ return () => Task.FromResult(actionExecutedContext);
+ }
+
+ ///
+ /// Since we could be returning the current page from a surface controller posted values in which the routing values are changed, we
+ /// need to revert these values back to nothing in order for the normal page to render again.
+ ///
+ private static void ResetRouteData(RouteData routeData)
+ {
+ routeData.DataTokens["area"] = null;
+ routeData.DataTokens["Namespaces"] = null;
+ }
+
+ ///
+ /// Validate that the current page execution is not being handled by the normal umbraco routing system
+ ///
+ private static void ValidateRouteData(RouteData routeData)
+ {
+ if (routeData.DataTokens.ContainsKey(Constants.Web.UmbracoRouteDefinitionDataToken) == false)
+ {
+ throw new InvalidOperationException("Can only use " + typeof(UmbracoPageResult).Name +
+ " in the context of an Http POST when using a SurfaceController form");
+ }
+ }
+
+ ///
+ /// Ensure ModelState, ViewData and TempData is copied across
+ ///
+ private static void CopyControllerData(ControllerContext context, Controller controller)
+ {
+ controller.ViewData.ModelState.Merge(context.ModelState);
+
+ foreach (var d in controller.ViewData)
+ controller.ViewData[d.Key] = d.Value;
+
+ // We cannot simply merge the temp data because during controller execution it will attempt to 'load' temp data
+ // but since it has not been saved, there will be nothing to load and it will revert to nothing, so the trick is
+ // to Save the state of the temp data first then it will automatically be picked up.
+ // http://issues.umbraco.org/issue/U4-1339
+
+ var targetController = controller;
+ var tempData = context.HttpContext.RequestServices.GetRequiredService();
+
+ targetController.TempData = tempData;
+ targetController.TempData.Save();
+ }
+
+ ///
+ /// Creates a controller using the controller factory
+ ///
+ private static Controller CreateController(ControllerContext context, IControllerFactory factory, RouteDefinition routeDef)
+ {
+ if (!(factory.CreateController(context) is Controller controller))
+ throw new InvalidOperationException("Could not create controller with name " + routeDef.ControllerName + ".");
+
+ return controller;
+ }
+
+ ///
+ /// Cleans up the controller by releasing it using the controller factory, and by disposing it.
+ ///
+ private static void CleanupController(ControllerContext context, Controller controller, IControllerFactory factory)
+ {
+ if (!(controller is null))
+ factory.ReleaseController(context, controller);
+
+ controller?.DisposeIfDisposable();
+ }
+
+ private class DummyView : IView
+ {
+ public DummyView(string path)
+ {
+ Path = path;
+ }
+
+ public Task RenderAsync(ViewContext context)
+ {
+ return Task.CompletedTask;
+ }
+
+ public string Path { get; }
+ }
+ }
+}
diff --git a/src/Umbraco.Web.Website/RouteDefinition.cs b/src/Umbraco.Web.Website/RouteDefinition.cs
new file mode 100644
index 0000000000..02eab6ae77
--- /dev/null
+++ b/src/Umbraco.Web.Website/RouteDefinition.cs
@@ -0,0 +1,29 @@
+using System;
+using Umbraco.Web.Routing;
+
+namespace Umbraco.Web.Website
+{
+ ///
+ /// Represents the data required to route to a specific controller/action during an Umbraco request
+ ///
+ public class RouteDefinition
+ {
+ public string ControllerName { get; set; }
+ public string ActionName { get; set; }
+
+ ///
+ /// The Controller type found for routing to
+ ///
+ public Type ControllerType { get; set; }
+
+ ///
+ /// Everything related to the current content request including the requested content
+ ///
+ public IPublishedRequest PublishedRequest { get; set; }
+
+ ///
+ /// Gets/sets whether the current request has a hijacked route/user controller routed for it
+ ///
+ public bool HasHijackedRoute { get; set; }
+ }
+}