From c024db9d3c005d81286591c73c0f68bc2f1e6020 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 2 Feb 2021 14:48:01 +1100 Subject: [PATCH] gets surface controllers and front-end api controllers auto-routed, adds tests --- .../Routing/FrontEndRouteTests.cs | 70 +++++++++ .../Routing/BackOfficeAreaRoutes.cs | 26 +++- .../Routing/PreviewRoutes.cs | 4 +- .../Controllers/UmbracoApiController.cs | 7 +- .../Controllers/UmbracoApiControllerBase.cs | 8 +- .../EndpointRouteBuilderExtensions.cs | 13 +- .../Extensions/LinkGeneratorExtensions.cs | 144 ++++++++++-------- .../Controllers/SurfaceController.cs | 43 ++---- .../UmbracoBuilderExtensions.cs | 4 +- .../Extensions/HtmlHelperRenderExtensions.cs | 31 ++-- .../Extensions/LinkGeneratorExtensions.cs | 52 +++++++ ...racoWebsiteApplicationBuilderExtensions.cs | 3 + .../Routing/FrontEndRoutes.cs | 105 +++++++++++++ 13 files changed, 367 insertions(+), 143 deletions(-) create mode 100644 src/Umbraco.Tests.Integration/Umbraco.Web.Website/Routing/FrontEndRouteTests.cs create mode 100644 src/Umbraco.Web.Website/Extensions/LinkGeneratorExtensions.cs create mode 100644 src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.Website/Routing/FrontEndRouteTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.Website/Routing/FrontEndRouteTests.cs new file mode 100644 index 0000000000..a7cbc1646b --- /dev/null +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.Website/Routing/FrontEndRouteTests.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Core.Services; +using Umbraco.Tests.Integration.TestServerTest; +using Umbraco.Web; +using Umbraco.Web.Routing; +using Umbraco.Web.Website.Controllers; + +namespace Umbraco.Tests.Integration.Umbraco.Web.Website.Routing +{ + [TestFixture] + public class SurfaceControllerTests : UmbracoTestServerTestBase + { + [Test] + public async Task Auto_Routes_For_Default_Action() + { + string url = PrepareSurfaceControllerUrl(x => x.Index()); + + // Act + HttpResponseMessage response = await Client.GetAsync(url); + + string body = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + } + + [Test] + public async Task Auto_Routes_For_Custom_Action() + { + string url = PrepareSurfaceControllerUrl(x => x.News()); + + // Act + HttpResponseMessage response = await Client.GetAsync(url); + + string body = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); + } + } + + // Test controllers must be non-nested, else we need to jump through some hoops with custom + // IApplicationFeatureProvider + // For future notes if we want this, some example code of this is here + // https://tpodolak.com/blog/2020/06/22/asp-net-core-adding-controllers-directly-integration-tests/ + public class TestSurfaceController : SurfaceController + { + public TestSurfaceController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider) + : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) + { + } + + public IActionResult Index() => Ok(); + + public IActionResult News() => Forbid(); + } +} diff --git a/src/Umbraco.Web.BackOffice/Routing/BackOfficeAreaRoutes.cs b/src/Umbraco.Web.BackOffice/Routing/BackOfficeAreaRoutes.cs index 39cfe0002b..56b5d26d92 100644 --- a/src/Umbraco.Web.BackOffice/Routing/BackOfficeAreaRoutes.cs +++ b/src/Umbraco.Web.BackOffice/Routing/BackOfficeAreaRoutes.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; using Umbraco.Core; @@ -8,6 +9,7 @@ using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Extensions; using Umbraco.Web.Common.Routing; +using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; namespace Umbraco.Web.BackOffice.Routing @@ -15,7 +17,7 @@ namespace Umbraco.Web.BackOffice.Routing /// /// Creates routes for the back office area /// - public class BackOfficeAreaRoutes : IAreaRoutes + public sealed class BackOfficeAreaRoutes : IAreaRoutes { private readonly GlobalSettings _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; @@ -23,6 +25,9 @@ namespace Umbraco.Web.BackOffice.Routing private readonly UmbracoApiControllerTypeCollection _apiControllers; private readonly string _umbracoPathSegment; + /// + /// Initializes a new instance of the class. + /// public BackOfficeAreaRoutes( IOptions globalSettings, IHostingEnvironment hostingEnvironment, @@ -36,6 +41,7 @@ namespace Umbraco.Web.BackOffice.Routing _umbracoPathSegment = _globalSettings.GetUmbracoMvcArea(_hostingEnvironment); } + /// public void CreateRoutes(IEndpointRouteBuilder endpoints) { switch (_runtimeState.Level) @@ -50,7 +56,7 @@ namespace Umbraco.Web.BackOffice.Routing case RuntimeLevel.Run: MapMinimalBackOffice(endpoints); - AutoRouteBackOfficeControllers(endpoints); + AutoRouteBackOfficeApiControllers(endpoints); break; case RuntimeLevel.BootFailed: case RuntimeLevel.Unknown: @@ -85,26 +91,30 @@ namespace Umbraco.Web.BackOffice.Routing } /// - /// Auto-routes all back office controllers + /// Auto-routes all back office api controllers /// - private void AutoRouteBackOfficeControllers(IEndpointRouteBuilder endpoints) + private void AutoRouteBackOfficeApiControllers(IEndpointRouteBuilder endpoints) { // TODO: We could investigate dynamically routing plugin controllers so we don't have to eagerly type scan for them, // it would probably work well, see https://www.strathweb.com/2019/08/dynamic-controller-routing-in-asp-net-core-3-0/ // will probably be what we use for front-end routing too. BTW the orig article about migrating from IRouter to endpoint // routing for things like a CMS is here https://github.com/dotnet/aspnetcore/issues/4221 - foreach (var controller in _apiControllers) + foreach (Type controller in _apiControllers) { + PluginControllerMetadata meta = PluginController.GetMetadata(controller); + // exclude front-end api controllers - var meta = PluginController.GetMetadata(controller); - if (!meta.IsBackOffice) continue; + if (!meta.IsBackOffice) + { + continue; + } endpoints.MapUmbracoApiRoute( meta.ControllerType, _umbracoPathSegment, meta.AreaName, - true, + meta.IsBackOffice, defaultAction: string.Empty); // no default action (this is what we had before) } } diff --git a/src/Umbraco.Web.BackOffice/Routing/PreviewRoutes.cs b/src/Umbraco.Web.BackOffice/Routing/PreviewRoutes.cs index 947e7ac468..d8c93e5985 100644 --- a/src/Umbraco.Web.BackOffice/Routing/PreviewRoutes.cs +++ b/src/Umbraco.Web.BackOffice/Routing/PreviewRoutes.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; using Umbraco.Core; @@ -15,7 +15,7 @@ namespace Umbraco.Web.BackOffice.Routing /// /// Creates routes for the preview hub /// - public class PreviewRoutes : IAreaRoutes + public sealed class PreviewRoutes : IAreaRoutes { private readonly IRuntimeState _runtimeState; private readonly string _umbracoPathSegment; diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs index e3250c2983..019e3cffdd 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs @@ -1,4 +1,4 @@ -using Umbraco.Core.Composing; +using Umbraco.Core.Composing; namespace Umbraco.Web.Common.Controllers { @@ -7,8 +7,9 @@ namespace Umbraco.Web.Common.Controllers /// public abstract class UmbracoApiController : UmbracoApiControllerBase, IDiscoverable { - // TODO: Should this only exist in the back office project? These really are only ever used for the back office AFAIK - + /// + /// Initializes a new instance of the class. + /// protected UmbracoApiController() { } diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs index 364c3c1211..811b0dfd69 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Authorization; -using Umbraco.Web.Common.Filters; using Umbraco.Web.Features; namespace Umbraco.Web.Common.Controllers @@ -18,9 +17,10 @@ namespace Umbraco.Web.Common.Controllers [UmbracoApiController] public abstract class UmbracoApiControllerBase : ControllerBase, IUmbracoFeature { - // TODO: Should this only exist in the back office project? These really are only ever used for the back office AFAIK - - public UmbracoApiControllerBase() + /// + /// Initializes a new instance of the class. + /// + protected UmbracoApiControllerBase() { } } diff --git a/src/Umbraco.Web.Common/Extensions/EndpointRouteBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/EndpointRouteBuilderExtensions.cs index ccaa29544b..37495c5ff5 100644 --- a/src/Umbraco.Web.Common/Extensions/EndpointRouteBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/EndpointRouteBuilderExtensions.cs @@ -29,21 +29,20 @@ namespace Umbraco.Web.Common.Extensions var pattern = new StringBuilder(rootSegment); if (!prefixPathSegment.IsNullOrWhiteSpace()) { - pattern.Append("/").Append(prefixPathSegment); + pattern.Append('/').Append(prefixPathSegment); } if (includeControllerNameInRoute) { - pattern.Append("/").Append(controllerName); + pattern.Append('/').Append(controllerName); } - pattern.Append("/").Append("{action}/{id?}"); + pattern.Append('/').Append("{action}/{id?}"); var defaults = defaultAction.IsNullOrWhiteSpace() ? (object)new { controller = controllerName } : new { controller = controllerName, action = defaultAction }; - if (areaName.IsNullOrWhiteSpace()) { endpoints.MapControllerRoute( @@ -70,6 +69,7 @@ namespace Umbraco.Web.Common.Extensions /// /// Used to map Umbraco controllers consistently /// + /// The type to route public static void MapUmbracoRoute( this IEndpointRouteBuilder endpoints, string rootSegment, @@ -82,8 +82,9 @@ namespace Umbraco.Web.Common.Extensions => endpoints.MapUmbracoRoute(typeof(T), rootSegment, areaName, prefixPathSegment, defaultAction, includeControllerNameInRoute, constraints); /// - /// Used to map Umbraco api controllers consistently + /// Used to map controllers as Umbraco API routes consistently /// + /// The type to route public static void MapUmbracoApiRoute( this IEndpointRouteBuilder endpoints, string rootSegment, @@ -95,7 +96,7 @@ namespace Umbraco.Web.Common.Extensions => endpoints.MapUmbracoApiRoute(typeof(T), rootSegment, areaName, isBackOffice, defaultAction, constraints); /// - /// Used to map Umbraco api controllers consistently + /// Used to map controllers as Umbraco API routes consistently /// public static void MapUmbracoApiRoute( this IEndpointRouteBuilder endpoints, diff --git a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs index 6bfa402154..e0c94efb83 100644 --- a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs @@ -2,13 +2,15 @@ using System; using System.Collections.Generic; using System.Dynamic; using System.Linq; -using Umbraco.Core; -using Microsoft.AspNetCore.Routing; -using System.Reflection; -using Umbraco.Web.Common.Install; -using Umbraco.Core.Hosting; using System.Linq.Expressions; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Umbraco.Core; +using Umbraco.Core.Hosting; using Umbraco.Web.Common.Controllers; +using Umbraco.Web.Common.Install; +using Umbraco.Web.Mvc; namespace Umbraco.Extensions { @@ -17,8 +19,6 @@ namespace Umbraco.Extensions /// /// Return the back office url if the back office is installed /// - /// - /// public static string GetBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment) { @@ -26,7 +26,10 @@ namespace Umbraco.Extensions try { backOfficeControllerType = Assembly.Load("Umbraco.Web.BackOffice")?.GetType("Umbraco.Web.BackOffice.Controllers.BackOfficeController"); - if (backOfficeControllerType == null) return "/"; // this would indicate that the installer is installed without the back office + if (backOfficeControllerType == null) + { + return "/"; // this would indicate that the installer is installed without the back office + } } catch { @@ -39,47 +42,33 @@ namespace Umbraco.Extensions /// /// Returns the URL for the installer /// - /// - /// public static string GetInstallerUrl(this LinkGenerator linkGenerator) - { - return linkGenerator.GetPathByAction(nameof(InstallController.Index), ControllerExtensions.GetControllerName(), new { area = Constants.Web.Mvc.InstallArea }); - } + => linkGenerator.GetPathByAction(nameof(InstallController.Index), ControllerExtensions.GetControllerName(), new { area = Constants.Web.Mvc.InstallArea }); /// /// Returns the URL for the installer api /// - /// - /// public static string GetInstallerApiUrl(this LinkGenerator linkGenerator) - { - return linkGenerator.GetPathByAction(nameof(InstallApiController.GetSetup), + => linkGenerator.GetPathByAction( + nameof(InstallApiController.GetSetup), ControllerExtensions.GetControllerName(), new { area = Constants.Web.Mvc.InstallArea }).TrimEnd(nameof(InstallApiController.GetSetup)); - } /// /// Return the Url for a Web Api service /// - /// - /// - /// - /// - /// + /// The public static string GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, object id = null) - where T : UmbracoApiControllerBase - { - return linkGenerator.GetUmbracoApiService(actionName, typeof(T), new Dictionary() - { - ["id"] = id - }); - } + where T : UmbracoApiControllerBase => linkGenerator.GetUmbracoControllerUrl( + actionName, + typeof(T), + new Dictionary() + { + ["id"] = id + }); public static string GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, IDictionary values) - where T : UmbracoApiControllerBase - { - return linkGenerator.GetUmbracoApiService(actionName, typeof(T), values); - } + where T : UmbracoApiControllerBase => linkGenerator.GetUmbracoControllerUrl(actionName, typeof(T), values); public static string GetUmbracoApiServiceBaseUrl(this LinkGenerator linkGenerator, Expression> methodSelector) where T : UmbracoApiControllerBase @@ -93,66 +82,86 @@ namespace Umbraco.Extensions } /// - /// Return the Url for a Web Api service + /// Return the Url for an Umbraco controller /// - /// - /// - /// - /// - /// - /// - public static string GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, string controllerName, string area, IDictionary dict = null) + public static string GetUmbracoControllerUrl(this LinkGenerator linkGenerator, string actionName, string controllerName, string area, IDictionary dict = null) { - if (actionName == null) throw new ArgumentNullException(nameof(actionName)); - if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); - if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); - if (string.IsNullOrWhiteSpace(controllerName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(controllerName)); + if (actionName == null) + { + throw new ArgumentNullException(nameof(actionName)); + } + + if (string.IsNullOrWhiteSpace(actionName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); + } + + if (controllerName == null) + { + throw new ArgumentNullException(nameof(controllerName)); + } + + if (string.IsNullOrWhiteSpace(controllerName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(controllerName)); + } if (dict is null) { dict = new Dictionary(); } - - if (!area.IsNullOrWhiteSpace()) { dict["area"] = area; } - - var values = dict.Aggregate(new ExpandoObject() as IDictionary, - (a, p) => { a.Add(p.Key, p.Value); return a; }); + IDictionary values = dict.Aggregate( + new ExpandoObject() as IDictionary, + (a, p) => + { + a.Add(p.Key, p.Value); + return a; + }); return linkGenerator.GetPathByAction(actionName, controllerName, values); } /// - /// Return the Url for a Web Api service + /// Return the Url for an Umbraco controller /// - /// - /// - /// - /// - /// - public static string GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, Type apiControllerType, IDictionary values = null) + public static string GetUmbracoControllerUrl(this LinkGenerator linkGenerator, string actionName, Type controllerType, IDictionary values = null) { - if (actionName == null) throw new ArgumentNullException(nameof(actionName)); - if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); - if (apiControllerType == null) throw new ArgumentNullException(nameof(apiControllerType)); + if (actionName == null) + { + throw new ArgumentNullException(nameof(actionName)); + } - var area = ""; + if (string.IsNullOrWhiteSpace(actionName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); + } - if (!typeof(UmbracoApiControllerBase).IsAssignableFrom(apiControllerType)) - throw new InvalidOperationException($"The controller {apiControllerType} is of type {typeof(UmbracoApiControllerBase)}"); + if (controllerType == null) + { + throw new ArgumentNullException(nameof(controllerType)); + } - var metaData = PluginController.GetMetadata(apiControllerType); + var area = string.Empty; + + if (!typeof(ControllerBase).IsAssignableFrom(controllerType)) + { + throw new InvalidOperationException($"The controller {controllerType} is of type {typeof(ControllerBase)}"); + } + + PluginControllerMetadata metaData = PluginController.GetMetadata(controllerType); if (metaData.AreaName.IsNullOrWhiteSpace() == false) { - //set the area to the plugin area + // set the area to the plugin area area = metaData.AreaName; } - return linkGenerator.GetUmbracoApiService(actionName, ControllerExtensions.GetControllerName(apiControllerType), area, values); + + return linkGenerator.GetUmbracoControllerUrl(actionName, ControllerExtensions.GetControllerName(controllerType), area, values); } public static string GetUmbracoApiService(this LinkGenerator linkGenerator, Expression> methodSelector) @@ -170,6 +179,7 @@ namespace Umbraco.Extensions { return linkGenerator.GetUmbracoApiService(method.Name); } + return linkGenerator.GetUmbracoApiService(method.Name, methodParams); } } diff --git a/src/Umbraco.Web.Website/Controllers/SurfaceController.cs b/src/Umbraco.Web.Website/Controllers/SurfaceController.cs index 3a6a7a3507..edf5428f9b 100644 --- a/src/Umbraco.Web.Website/Controllers/SurfaceController.cs +++ b/src/Umbraco.Web.Website/Controllers/SurfaceController.cs @@ -22,11 +22,12 @@ namespace Umbraco.Web.Website.Controllers // [MergeParentContextViewData] public abstract class SurfaceController : PluginController { + /// + /// Initializes a new instance of the class. + /// protected SurfaceController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider) : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger) - { - PublishedUrlProvider = publishedUrlProvider; - } + => PublishedUrlProvider = publishedUrlProvider; protected IPublishedUrlProvider PublishedUrlProvider { get; } @@ -52,49 +53,37 @@ namespace Umbraco.Web.Website.Controllers /// Redirects to the Umbraco page with the given id /// protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid contentKey) - { - return new RedirectToUmbracoPageResult(contentKey, PublishedUrlProvider, UmbracoContextAccessor); - } + => new RedirectToUmbracoPageResult(contentKey, PublishedUrlProvider, UmbracoContextAccessor); /// /// Redirects to the Umbraco page with the given id and passes provided querystring /// protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid contentKey, QueryString queryString) - { - return new RedirectToUmbracoPageResult(contentKey, queryString, PublishedUrlProvider, UmbracoContextAccessor); - } + => new RedirectToUmbracoPageResult(contentKey, queryString, PublishedUrlProvider, UmbracoContextAccessor); /// /// Redirects to the Umbraco page with the given published content /// protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent) - { - return new RedirectToUmbracoPageResult(publishedContent, PublishedUrlProvider, UmbracoContextAccessor); - } + => new RedirectToUmbracoPageResult(publishedContent, PublishedUrlProvider, UmbracoContextAccessor); /// /// Redirects to the Umbraco page with the given published content and passes provided querystring /// protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent, QueryString queryString) - { - return new RedirectToUmbracoPageResult(publishedContent, queryString, PublishedUrlProvider, UmbracoContextAccessor); - } + => new RedirectToUmbracoPageResult(publishedContent, queryString, PublishedUrlProvider, UmbracoContextAccessor); /// /// Redirects to the currently rendered Umbraco page /// protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage() - { - return new RedirectToUmbracoPageResult(CurrentPage, PublishedUrlProvider, UmbracoContextAccessor); - } + => new RedirectToUmbracoPageResult(CurrentPage, PublishedUrlProvider, UmbracoContextAccessor); /// /// Redirects to the currently rendered Umbraco page and passes provided querystring /// protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage(QueryString queryString) - { - return new RedirectToUmbracoPageResult(CurrentPage, queryString, PublishedUrlProvider, UmbracoContextAccessor); - } + => new RedirectToUmbracoPageResult(CurrentPage, queryString, PublishedUrlProvider, UmbracoContextAccessor); /// /// Redirects to the currently rendered Umbraco URL @@ -105,17 +94,13 @@ namespace Umbraco.Web.Website.Controllers /// Server.Transfer.* /// protected RedirectToUmbracoUrlResult RedirectToCurrentUmbracoUrl() - { - return new RedirectToUmbracoUrlResult(UmbracoContext); - } + => new RedirectToUmbracoUrlResult(UmbracoContext); /// /// Returns the currently rendered Umbraco page /// protected UmbracoPageResult CurrentUmbracoPage() - { - return new UmbracoPageResult(ProfilingLogger); - } + => new UmbracoPageResult(ProfilingLogger); /// /// we need to recursively find the route definition based on the parent view context @@ -126,9 +111,9 @@ namespace Umbraco.Web.Website.Controllers while (!(currentContext is null)) { var currentRouteData = currentContext.RouteData; - if (currentRouteData.Values.ContainsKey(Core.Constants.Web.UmbracoRouteDefinitionDataToken)) + if (currentRouteData.Values.ContainsKey(Constants.Web.UmbracoRouteDefinitionDataToken)) { - return Attempt.Succeed((UmbracoRouteValues)currentRouteData.Values[Core.Constants.Web.UmbracoRouteDefinitionDataToken]); + return Attempt.Succeed((UmbracoRouteValues)currentRouteData.Values[Constants.Web.UmbracoRouteDefinitionDataToken]); } } diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs index 3d0c482a30..42174ecb73 100644 --- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs @@ -1,11 +1,9 @@ using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Core.DependencyInjection; using Umbraco.Extensions; using Umbraco.Infrastructure.DependencyInjection; -using Umbraco.Infrastructure.PublishedCache.DependencyInjection; using Umbraco.ModelsBuilder.Embedded.DependencyInjection; using Umbraco.Web.Common.Routing; using Umbraco.Web.Website.Collections; @@ -44,6 +42,8 @@ namespace Umbraco.Web.Website.DependencyInjection builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder .AddDistributedCache() .AddModelsBuilder(); diff --git a/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs index 65da2ca365..f79648b19a 100644 --- a/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs @@ -35,34 +35,20 @@ namespace Umbraco.Extensions public static class HtmlHelperRenderExtensions { private static T GetRequiredService(IHtmlHelper htmlHelper) - { - return GetRequiredService(htmlHelper.ViewContext); - } + => GetRequiredService(htmlHelper.ViewContext); private static T GetRequiredService(ViewContext viewContext) - { - return viewContext.HttpContext.RequestServices.GetRequiredService(); - } + => viewContext.HttpContext.RequestServices.GetRequiredService(); /// /// Renders the markup for the profiler /// - /// - /// public static IHtmlContent RenderProfiler(this IHtmlHelper helper) - { - return new HtmlString(GetRequiredService(helper).Render()); - } + => new HtmlString(GetRequiredService(helper).Render()); /// /// Renders a partial view that is found in the specified area /// - /// - /// - /// - /// - /// - /// public static IHtmlContent AreaPartial(this IHtmlHelper helper, string partial, string area, object model = null, ViewDataDictionary viewData = null) { var originalArea = helper.ViewContext.RouteData.DataTokens["area"]; @@ -76,8 +62,6 @@ namespace Umbraco.Extensions /// Will render the preview badge when in preview mode which is not required ever unless the MVC page you are /// using does not inherit from UmbracoViewPage /// - /// - /// /// /// See: http://issues.umbraco.org/issue/U4-1614 /// @@ -109,9 +93,9 @@ namespace Umbraco.Extensions Func contextualKeyBuilder = null) { var cacheKey = new StringBuilder(partialViewName); - //let's always cache by the current culture to allow variants to have different cache results + // let's always cache by the current culture to allow variants to have different cache results var cultureName = System.Threading.Thread.CurrentThread.CurrentUICulture.Name; - if (!String.IsNullOrEmpty(cultureName)) + if (!string.IsNullOrEmpty(cultureName)) { cacheKey.AppendFormat("{0}-", cultureName); } @@ -123,16 +107,19 @@ namespace Umbraco.Extensions { throw new InvalidOperationException("Cannot cache by page if the UmbracoContext has not been initialized, this parameter can only be used in the context of an Umbraco request"); } + cacheKey.AppendFormat("{0}-", umbracoContext.PublishedRequest?.PublishedContent?.Id ?? 0); } + if (cacheByMember) { - //TODO reintroduce when members are migrated + // TODO reintroduce when members are migrated throw new NotImplementedException("Reintroduce when members are migrated"); // var helper = Current.MembershipHelper; // var currentMember = helper.GetCurrentMember(); // cacheKey.AppendFormat("m{0}-", currentMember?.Id ?? 0); } + if (contextualKeyBuilder != null) { var contextualKey = contextualKeyBuilder(model, viewData); diff --git a/src/Umbraco.Web.Website/Extensions/LinkGeneratorExtensions.cs b/src/Umbraco.Web.Website/Extensions/LinkGeneratorExtensions.cs new file mode 100644 index 0000000000..86f90c5a97 --- /dev/null +++ b/src/Umbraco.Web.Website/Extensions/LinkGeneratorExtensions.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.AspNetCore.Routing; +using Umbraco.Core; +using Umbraco.Web.Website.Controllers; + +namespace Umbraco.Extensions +{ + public static class LinkGeneratorExtensions + { + /// + /// Return the Url for a Surface Controller + /// + /// The + public static string GetUmbracoSurfaceUrl(this LinkGenerator linkGenerator, Expression> methodSelector) + where T : SurfaceController + { + MethodInfo method = ExpressionHelper.GetMethodInfo(methodSelector); + IDictionary methodParams = ExpressionHelper.GetMethodParams(methodSelector); + + if (method == null) + { + throw new MissingMethodException( + $"Could not find the method {methodSelector} on type {typeof(T)} or the result "); + } + + if (methodParams.Any() == false) + { + return linkGenerator.GetUmbracoSurfaceUrl(method.Name); + } + + return linkGenerator.GetUmbracoSurfaceUrl(method.Name, methodParams); + } + + /// + /// Return the Url for a Surface Controller + /// + /// The + public static string GetUmbracoSurfaceUrl(this LinkGenerator linkGenerator, string actionName, object id = null) + where T : SurfaceController => linkGenerator.GetUmbracoControllerUrl( + actionName, + typeof(T), + new Dictionary() + { + ["id"] = id + }); + } +} diff --git a/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs b/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs index 36e5ff9214..af7041011c 100644 --- a/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs @@ -37,6 +37,9 @@ namespace Umbraco.Extensions { app.UseEndpoints(endpoints => { + FrontEndRoutes surfaceRoutes = app.ApplicationServices.GetRequiredService(); + surfaceRoutes.CreateRoutes(endpoints); + endpoints.MapDynamicControllerRoute("/{**slug}"); }); diff --git a/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs b/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs new file mode 100644 index 0000000000..67ce14e7aa --- /dev/null +++ b/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Options; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Hosting; +using Umbraco.Web.Common.Controllers; +using Umbraco.Web.Common.Extensions; +using Umbraco.Web.Common.Routing; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Umbraco.Web.Website.Collections; + +namespace Umbraco.Web.Website.Routing +{ + /// + /// Creates routes for surface controllers + /// + public sealed class FrontEndRoutes : IAreaRoutes + { + private readonly GlobalSettings _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IRuntimeState _runtimeState; + private readonly SurfaceControllerTypeCollection _surfaceControllerTypeCollection; + private readonly UmbracoApiControllerTypeCollection _apiControllers; + private readonly string _umbracoPathSegment; + + /// + /// Initializes a new instance of the class. + /// + public FrontEndRoutes( + IOptions globalSettings, + IHostingEnvironment hostingEnvironment, + IRuntimeState runtimeState, + SurfaceControllerTypeCollection surfaceControllerTypeCollection, + UmbracoApiControllerTypeCollection apiControllers) + { + _globalSettings = globalSettings.Value; + _hostingEnvironment = hostingEnvironment; + _runtimeState = runtimeState; + _surfaceControllerTypeCollection = surfaceControllerTypeCollection; + _apiControllers = apiControllers; + _umbracoPathSegment = _globalSettings.GetUmbracoMvcArea(_hostingEnvironment); + } + + /// + public void CreateRoutes(IEndpointRouteBuilder endpoints) + { + if (_runtimeState.Level != RuntimeLevel.Run) + { + return; + } + + AutoRouteSurfaceControllers(endpoints); + AutoRouteFrontEndApiControllers(endpoints); + } + + /// + /// Auto-routes all front-end surface controllers + /// + private void AutoRouteSurfaceControllers(IEndpointRouteBuilder endpoints) + { + foreach (Type controller in _surfaceControllerTypeCollection) + { + // exclude front-end api controllers + PluginControllerMetadata meta = PluginController.GetMetadata(controller); + + endpoints.MapUmbracoRoute( + meta.ControllerType, + _umbracoPathSegment, + meta.AreaName, + "Surface"); + } + } + + /// + /// Auto-routes all front-end api controllers + /// + private void AutoRouteFrontEndApiControllers(IEndpointRouteBuilder endpoints) + { + foreach (Type controller in _apiControllers) + { + PluginControllerMetadata meta = PluginController.GetMetadata(controller); + + // exclude back-end api controllers + if (meta.IsBackOffice) + { + continue; + } + + endpoints.MapUmbracoApiRoute( + meta.ControllerType, + _umbracoPathSegment, + meta.AreaName, + meta.IsBackOffice, + defaultAction: string.Empty); // no default action (this is what we had before) + } + } + } +}