From e3be4009c02e941f2a89411598c91a8cc3321a4e Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 8 Dec 2020 16:33:50 +1100 Subject: [PATCH 01/14] Getting front end routing poc going --- src/Umbraco.Core/Routing/PublishedRouter.cs | 7 +- .../{ => Web}/HybridUmbracoContextAccessor.cs | 0 src/Umbraco.Core/{ => Web}/IUmbracoContext.cs | 0 .../{ => Web}/IUmbracoContextAccessor.cs | 0 .../{ => Web}/IUmbracoContextFactory.cs | 0 ...RenderIndexActionSelectorAttributeTests.cs | 17 +- .../Controllers/SurfaceControllerTests.cs | 1 + .../Controllers/BackOfficeController.cs | 2 +- .../BackOfficeApplicationBuilderExtensions.cs | 49 ++-- .../PreviewAuthenticationMiddleware.cs | 6 +- .../Trees/ApplicationTreeController.cs | 34 +-- .../Controllers/IRenderController.cs | 6 +- .../Controllers/IRenderMvcController.cs | 19 -- .../Controllers/RenderController.cs | 10 - .../Controllers/UmbracoController.cs | 11 +- .../ApplicationBuilderExtensions.cs | 110 ++++++--- ...racoInstallApplicationBuilderExtensions.cs | 17 +- ...tialViewMacroViewContextFilterAttribute.cs | 35 +-- .../PreRenderViewActionFilterAttribute.cs | 43 ---- .../Middleware/UmbracoRequestMiddleware.cs | 22 +- src/Umbraco.Web.Common/Routing/IAreaRoutes.cs | 6 +- src/Umbraco.Web.UI.NetCore/Startup.cs | 1 + .../Controllers/IUmbracoRenderingDefaults.cs | 15 ++ ...erMvcController.cs => RenderController.cs} | 34 ++- .../RenderIndexActionSelectorAttribute.cs | 23 +- .../Controllers/SurfaceController.cs | 3 +- .../Controllers/UmbracoRenderingDefaults.cs | 13 + .../Extensions/UmbracoBuilderExtensions.cs | 17 +- ...racoWebsiteApplicationBuilderExtensions.cs | 31 ++- .../Routing/NoContentRoutes.cs | 19 +- .../{ => Routing}/RouteDefinition.cs | 4 +- .../Routing/UmbracoRouteValueTransformer.cs | 230 ++++++++++++++++++ .../Mvc/RenderControllerFactory.cs | 45 ---- src/Umbraco.Web/Mvc/RenderRouteHandler.cs | 10 +- src/Umbraco.Web/Mvc/RouteDefinition.cs | 8 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 - src/Umbraco.Web/UmbracoInjectedModule.cs | 9 +- 37 files changed, 543 insertions(+), 315 deletions(-) rename src/Umbraco.Core/{ => Web}/HybridUmbracoContextAccessor.cs (100%) rename src/Umbraco.Core/{ => Web}/IUmbracoContext.cs (100%) rename src/Umbraco.Core/{ => Web}/IUmbracoContextAccessor.cs (100%) rename src/Umbraco.Core/{ => Web}/IUmbracoContextFactory.cs (100%) delete mode 100644 src/Umbraco.Web.Common/Controllers/IRenderMvcController.cs delete mode 100644 src/Umbraco.Web.Common/Controllers/RenderController.cs delete mode 100644 src/Umbraco.Web.Common/Filters/PreRenderViewActionFilterAttribute.cs create mode 100644 src/Umbraco.Web.Website/Controllers/IUmbracoRenderingDefaults.cs rename src/Umbraco.Web.Website/Controllers/{RenderMvcController.cs => RenderController.cs} (78%) create mode 100644 src/Umbraco.Web.Website/Controllers/UmbracoRenderingDefaults.cs rename src/Umbraco.Web.Website/{ => Routing}/RouteDefinition.cs (94%) create mode 100644 src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs delete mode 100644 src/Umbraco.Web/Mvc/RenderControllerFactory.cs diff --git a/src/Umbraco.Core/Routing/PublishedRouter.cs b/src/Umbraco.Core/Routing/PublishedRouter.cs index 9b7354de74..10986b941a 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -138,7 +138,7 @@ namespace Umbraco.Web.Routing Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = request.Culture; SetVariationContext(request.Culture.Name); - //find the published content if it's not assigned. This could be manually assigned with a custom route handler, or + // find the published content if it's not assigned. This could be manually assigned with a custom route handler, or // with something like EnsurePublishedContentRequestAttribute or UmbracoVirtualNodeRouteHandler. Those in turn call this method // to setup the rest of the pipeline but we don't want to run the finders since there's one assigned. if (request.PublishedContent == null) @@ -156,15 +156,13 @@ namespace Umbraco.Web.Routing // trigger the Prepared event - at that point it is still possible to change about anything // even though the request might be flagged for redirection - we'll redirect _after_ the event - // // also, OnPrepared() will make the PublishedRequest readonly, so nothing can change - // request.OnPrepared(); // we don't take care of anything so if the content has changed, it's up to the user // to find out the appropriate template - //complete the PCR and assign the remaining values + // complete the PCR and assign the remaining values return ConfigureRequest(request); } @@ -201,7 +199,6 @@ namespace Umbraco.Web.Routing // can't go beyond that point without a PublishedContent to render // it's ok not to have a template, in order to give MVC a chance to hijack routes - return true; } diff --git a/src/Umbraco.Core/HybridUmbracoContextAccessor.cs b/src/Umbraco.Core/Web/HybridUmbracoContextAccessor.cs similarity index 100% rename from src/Umbraco.Core/HybridUmbracoContextAccessor.cs rename to src/Umbraco.Core/Web/HybridUmbracoContextAccessor.cs diff --git a/src/Umbraco.Core/IUmbracoContext.cs b/src/Umbraco.Core/Web/IUmbracoContext.cs similarity index 100% rename from src/Umbraco.Core/IUmbracoContext.cs rename to src/Umbraco.Core/Web/IUmbracoContext.cs diff --git a/src/Umbraco.Core/IUmbracoContextAccessor.cs b/src/Umbraco.Core/Web/IUmbracoContextAccessor.cs similarity index 100% rename from src/Umbraco.Core/IUmbracoContextAccessor.cs rename to src/Umbraco.Core/Web/IUmbracoContextAccessor.cs diff --git a/src/Umbraco.Core/IUmbracoContextFactory.cs b/src/Umbraco.Core/Web/IUmbracoContextFactory.cs similarity index 100% rename from src/Umbraco.Core/IUmbracoContextFactory.cs rename to src/Umbraco.Core/Web/IUmbracoContextFactory.cs diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttributeTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttributeTests.cs index 3a987fb038..bf5c422bd8 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttributeTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttributeTests.cs @@ -16,6 +16,7 @@ using Moq; using NUnit.Framework; using Umbraco.Web.Models; using Umbraco.Web.Mvc; +using Umbraco.Web.Website.Controllers; namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Controllers { @@ -118,48 +119,48 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Controllers public ActionDescriptorCollection ActionDescriptors { get; } } - private class MatchesDefaultIndexController : RenderMvcController + private class MatchesDefaultIndexController : RenderController { - public MatchesDefaultIndexController(ILogger logger, + public MatchesDefaultIndexController(ILogger logger, ICompositeViewEngine compositeViewEngine) : base(logger, compositeViewEngine) { } } - private class MatchesOverriddenIndexController : RenderMvcController + private class MatchesOverriddenIndexController : RenderController { public override IActionResult Index(ContentModel model) { return base.Index(model); } - public MatchesOverriddenIndexController(ILogger logger, + public MatchesOverriddenIndexController(ILogger logger, ICompositeViewEngine compositeViewEngine) : base(logger, compositeViewEngine) { } } - private class MatchesCustomIndexController : RenderMvcController + private class MatchesCustomIndexController : RenderController { public IActionResult Index(ContentModel model, int page) { return base.Index(model); } - public MatchesCustomIndexController(ILogger logger, + public MatchesCustomIndexController(ILogger logger, ICompositeViewEngine compositeViewEngine) : base(logger, compositeViewEngine) { } } - private class MatchesAsyncIndexController : RenderMvcController + private class MatchesAsyncIndexController : RenderController { public new async Task Index(ContentModel model) { return await Task.FromResult(base.Index(model)); } - public MatchesAsyncIndexController(ILogger logger, + public MatchesAsyncIndexController(ILogger logger, ICompositeViewEngine compositeViewEngine) : base(logger, compositeViewEngine) { } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs index 82bd6719d4..d1ffc2044e 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs @@ -19,6 +19,7 @@ using Umbraco.Web.Routing; using Umbraco.Web.Security; using Umbraco.Web.Website; using Umbraco.Web.Website.Controllers; +using Umbraco.Web.Website.Routing; using CoreConstants = Umbraco.Core.Constants; namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Controllers diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index 19fb6aa2df..34d3a96ca3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -30,10 +30,10 @@ using Umbraco.Web.BackOffice.Security; using Umbraco.Web.Common.ActionsResults; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Authorization; +using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Filters; using Umbraco.Web.Common.Security; using Umbraco.Web.Models; -using Umbraco.Web.Mvc; using Umbraco.Web.WebAssets; using Constants = Umbraco.Core.Constants; diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs index 6ff42a5737..fe4951bc2b 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs @@ -8,60 +8,49 @@ using Umbraco.Web.Common.Security; namespace Umbraco.Extensions { + /// + /// extensions for Umbraco + /// public static class BackOfficeApplicationBuilderExtensions { - public static IApplicationBuilder UseUmbraco(this IApplicationBuilder app) - { - if (app == null) throw new ArgumentNullException(nameof(app)); - app.UseStatusCodePages(); - app.UseRouting(); - - app.UseUmbracoCore(); - app.UseUmbracoRouting(); - app.UseRequestLocalization(); - app.UseUmbracoRequestLogging(); - app.UseUmbracoBackOffice(); - app.UseUmbracoPreview(); - app.UseUmbracoInstaller(); - - return app; - } - public static IApplicationBuilder UseUmbracoBackOffice(this IApplicationBuilder app) { - if (app == null) throw new ArgumentNullException(nameof(app)); + // NOTE: This method will have been called after UseRouting, UseAuthentication, UseAuthorization + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } app.UseBackOfficeUserManagerAuditing(); - // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. - // TODO: Since we are dependent on these we need to register them but what happens when we call this multiple times since we are dependent on this for UseUmbracoBackOffice too? - app.UseImageSharp(); - app.UseStaticFiles(); - - // Must be called after UseRouting and before UseEndpoints - app.UseSession(); - - if (!app.UmbracoCanBoot()) return app; + if (!app.UmbracoCanBoot()) + { + return app; + } app.UseEndpoints(endpoints => { - var backOfficeRoutes = app.ApplicationServices.GetRequiredService(); + BackOfficeAreaRoutes backOfficeRoutes = app.ApplicationServices.GetRequiredService(); backOfficeRoutes.CreateRoutes(endpoints); }); app.UseUmbracoRuntimeMinification(); - app.UseMiddleware(); app.UseMiddleware(); + app.UseUmbracoPreview(); + return app; } public static IApplicationBuilder UseUmbracoPreview(this IApplicationBuilder app) { + // TODO: I'm unsure this middleware will execute before the endpoint, we'll have to see + app.UseMiddleware(); + app.UseEndpoints(endpoints => { - var previewRoutes = app.ApplicationServices.GetRequiredService(); + PreviewRoutes previewRoutes = app.ApplicationServices.GetRequiredService(); previewRoutes.CreateRoutes(endpoints); }); diff --git a/src/Umbraco.Web.BackOffice/Middleware/PreviewAuthenticationMiddleware.cs b/src/Umbraco.Web.BackOffice/Middleware/PreviewAuthenticationMiddleware.cs index 284dbbc913..06715b4ad1 100644 --- a/src/Umbraco.Web.BackOffice/Middleware/PreviewAuthenticationMiddleware.cs +++ b/src/Umbraco.Web.BackOffice/Middleware/PreviewAuthenticationMiddleware.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -45,7 +45,7 @@ namespace Umbraco.Web.BackOffice.Middleware if (cookieOptions == null) throw new InvalidOperationException("No cookie options found with name " + Constants.Security.BackOfficeAuthenticationType); - //If we've gotten this far it means a preview cookie has been set and a front-end umbraco document request is executing. + // If we've gotten this far it means a preview cookie has been set and a front-end umbraco document request is executing. // In this case, authentication will not have occurred for an Umbraco back office User, however we need to perform the authentication // for the user here so that the preview capability can be authorized otherwise only the non-preview page will be rendered. if (request.Cookies.TryGetValue(cookieOptions.Cookie.Name, out var cookie)) @@ -55,7 +55,7 @@ namespace Umbraco.Web.BackOffice.Middleware { var backOfficeIdentity = unprotected.Principal.GetUmbracoIdentity(); if (backOfficeIdentity != null) - //Ok, we've got a real ticket, now we can add this ticket's identity to the current + // Ok, we've got a real ticket, now we can add this ticket's identity to the current // Principal, this means we'll have 2 identities assigned to the principal which we can // use to authorize the preview and allow for a back office User. diff --git a/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs index 2b9370fedd..000740e27e 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -46,13 +46,13 @@ namespace Umbraco.Web.BackOffice.Trees IControllerFactory controllerFactory, IActionDescriptorCollectionProvider actionDescriptorCollectionProvider ) - { + { _treeService = treeService; _sectionService = sectionService; _localizedTextService = localizedTextService; _controllerFactory = controllerFactory; _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; - } + } /// /// Returns the tree nodes for an application @@ -62,7 +62,7 @@ namespace Umbraco.Web.BackOffice.Trees /// /// Tree use. /// - public async Task GetApplicationTrees(string application, string tree, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings, TreeUse use = TreeUse.Main) + public async Task GetApplicationTrees(string application, string tree, [ModelBinder(typeof(HttpQueryStringModelBinder))] FormCollection queryStrings, TreeUse use = TreeUse.Main) { application = application.CleanForXss(); @@ -165,7 +165,8 @@ namespace Umbraco.Web.BackOffice.Trees /// private async Task TryGetRootNode(Tree tree, FormCollection querystring) { - if (tree == null) throw new ArgumentNullException(nameof(tree)); + if (tree == null) + throw new ArgumentNullException(nameof(tree)); try { @@ -185,7 +186,8 @@ namespace Umbraco.Web.BackOffice.Trees /// private async Task GetTreeRootNode(Tree tree, int id, FormCollection querystring) { - if (tree == null) throw new ArgumentNullException(nameof(tree)); + if (tree == null) + throw new ArgumentNullException(nameof(tree)); var children = await GetChildren(tree, id, querystring); var rootNode = await GetRootNode(tree, querystring); @@ -214,9 +216,10 @@ namespace Umbraco.Web.BackOffice.Trees /// private async Task GetRootNode(Tree tree, FormCollection querystring) { - if (tree == null) throw new ArgumentNullException(nameof(tree)); + if (tree == null) + throw new ArgumentNullException(nameof(tree)); - var controller = (TreeControllerBase) await GetApiControllerProxy(tree.TreeControllerType, "GetRootNode", querystring); + var controller = (TreeControllerBase)await GetApiControllerProxy(tree.TreeControllerType, "GetRootNode", querystring); var rootNode = controller.GetRootNode(querystring); if (rootNode == null) throw new InvalidOperationException($"Failed to get root node for tree \"{tree.TreeAlias}\"."); @@ -228,7 +231,8 @@ namespace Umbraco.Web.BackOffice.Trees /// private async Task GetChildren(Tree tree, int id, FormCollection querystring) { - if (tree == null) throw new ArgumentNullException(nameof(tree)); + if (tree == null) + throw new ArgumentNullException(nameof(tree)); // the method we proxy has an 'id' parameter which is *not* in the querystring, // we need to add it for the proxy to work (else, it does not find the method, @@ -237,7 +241,7 @@ namespace Umbraco.Web.BackOffice.Trees d["id"] = StringValues.Empty; var proxyQuerystring = new FormCollection(d); - var controller = (TreeControllerBase) await GetApiControllerProxy(tree.TreeControllerType, "GetNodes", proxyQuerystring); + var controller = (TreeControllerBase)await GetApiControllerProxy(tree.TreeControllerType, "GetNodes", proxyQuerystring); return controller.GetNodes(id.ToInvariantString(), querystring); } @@ -267,7 +271,7 @@ namespace Umbraco.Web.BackOffice.Trees }); if (!(querystring is null)) { - foreach (var (key,value) in querystring) + foreach (var (key, value) in querystring) { routeData.Values[key] = value; } @@ -281,11 +285,11 @@ namespace Umbraco.Web.BackOffice.Trees var actionContext = new ActionContext(HttpContext, routeData, actionDescriptor); var proxyControllerContext = new ControllerContext(actionContext); - var controller = (TreeController) _controllerFactory.CreateController(proxyControllerContext); + var controller = (TreeController)_controllerFactory.CreateController(proxyControllerContext); - var isAllowed = await controller.ControllerContext.InvokeAuthorizationFiltersForRequest(actionContext); - if (!isAllowed) - throw new HttpResponseException(HttpStatusCode.Forbidden); + var isAllowed = await controller.ControllerContext.InvokeAuthorizationFiltersForRequest(actionContext); + if (!isAllowed) + throw new HttpResponseException(HttpStatusCode.Forbidden); return controller; } diff --git a/src/Umbraco.Web.Common/Controllers/IRenderController.cs b/src/Umbraco.Web.Common/Controllers/IRenderController.cs index 7534abc9b4..26a1286afa 100644 --- a/src/Umbraco.Web.Common/Controllers/IRenderController.cs +++ b/src/Umbraco.Web.Common/Controllers/IRenderController.cs @@ -1,9 +1,11 @@ -namespace Umbraco.Web.Mvc +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Common.Controllers { /// /// A marker interface to designate that a controller will be used for Umbraco front-end requests and/or route hijacking /// - public interface IRenderController + public interface IRenderController : IDiscoverable { } diff --git a/src/Umbraco.Web.Common/Controllers/IRenderMvcController.cs b/src/Umbraco.Web.Common/Controllers/IRenderMvcController.cs deleted file mode 100644 index 8727918bf4..0000000000 --- a/src/Umbraco.Web.Common/Controllers/IRenderMvcController.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Umbraco.Core.Composing; -using Umbraco.Web.Models; - -namespace Umbraco.Web.Mvc -{ - /// - /// The interface that must be implemented for a controller to be designated to execute for route hijacking - /// - public interface IRenderMvcController : IRenderController, IDiscoverable - { - /// - /// The default action to render the front-end view - /// - /// - /// - IActionResult Index(ContentModel model); - } -} diff --git a/src/Umbraco.Web.Common/Controllers/RenderController.cs b/src/Umbraco.Web.Common/Controllers/RenderController.cs deleted file mode 100644 index b95859ccbe..0000000000 --- a/src/Umbraco.Web.Common/Controllers/RenderController.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Umbraco.Web.Mvc; - -namespace Umbraco.Web.Common.Controllers -{ - public abstract class RenderController : Controller, IRenderController - { - - } -} diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoController.cs b/src/Umbraco.Web.Common/Controllers/UmbracoController.cs index 50ec620741..22bef0da69 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoController.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoController.cs @@ -1,14 +1,7 @@ -using System; +using System; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Umbraco.Core.Cache; -using Umbraco.Core.Logging; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Services; -using Umbraco.Web.Security; -namespace Umbraco.Web.Mvc +namespace Umbraco.Web.Common.Controllers { /// /// Provides a base class for Umbraco controllers. diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index caf4132664..99a2b2aa3f 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Serilog.Context; +using SixLabors.ImageSharp.Web.DependencyInjection; using Smidge; using Smidge.Nuglify; using StackExchange.Profiling; @@ -14,16 +15,61 @@ using Umbraco.Web.Common.Middleware; namespace Umbraco.Extensions { + /// + /// extensions for Umbraco + /// public static class ApplicationBuilderExtensions { + /// + /// Configures and use services required for using Umbraco + /// + public static IApplicationBuilder UseUmbraco(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + app.UseUmbracoCore(); + app.UseUmbracoRequestLogging(); + + // We need to add this before UseRouting so that the UmbracoContext and other middlewares are executed + // before endpoint routing middleware. + app.UseUmbracoRouting(); + + app.UseStatusCodePages(); + + // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. + // TODO: Since we are dependent on these we need to register them but what happens when we call this multiple times since we are dependent on this for UseUmbracoBackOffice too? + app.UseImageSharp(); + app.UseStaticFiles(); + + // UseRouting adds endpoint routing middleware, this means that middlewares registered after this one + // will execute after endpoint routing. The ordering of everything is quite important here, see + // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-5.0 + // where we need to have UseAuthentication and UseAuthorization proceeding this call but before + // endpoints are defined. + app.UseRouting(); + app.UseRequestLocalization(); + app.UseAuthentication(); + app.UseAuthorization(); + + // Must be called after UseRouting and before UseEndpoints + app.UseSession(); + + // Must come after the above! + app.UseUmbracoInstaller(); + + return app; + } + /// /// Returns true if Umbraco is greater than /// - /// - /// public static bool UmbracoCanBoot(this IApplicationBuilder app) { - var runtime = app.ApplicationServices.GetRequiredService(); + IRuntime runtime = app.ApplicationServices.GetRequiredService(); + // can't continue if boot failed return runtime.State.Level > RuntimeLevel.BootFailed; } @@ -31,26 +77,30 @@ namespace Umbraco.Extensions /// /// Start Umbraco /// - /// - /// public static IApplicationBuilder UseUmbracoCore(this IApplicationBuilder app) { - if (app == null) throw new ArgumentNullException(nameof(app)); + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } - if (!app.UmbracoCanBoot()) return app; + if (!app.UmbracoCanBoot()) + { + return app; + } - var hostingEnvironment = app.ApplicationServices.GetRequiredService(); - AppDomain.CurrentDomain.SetData("DataDirectory", hostingEnvironment?.MapPathContentRoot(Core.Constants.SystemDirectories.Data)); + IHostingEnvironment hostingEnvironment = app.ApplicationServices.GetRequiredService(); + AppDomain.CurrentDomain.SetData("DataDirectory", hostingEnvironment?.MapPathContentRoot(Constants.SystemDirectories.Data)); - var runtime = app.ApplicationServices.GetRequiredService(); + IRuntime runtime = app.ApplicationServices.GetRequiredService(); // Register a listener for application shutdown in order to terminate the runtime - var hostLifetime = app.ApplicationServices.GetRequiredService(); + IApplicationShutdownRegistry hostLifetime = app.ApplicationServices.GetRequiredService(); var runtimeShutdown = new CoreRuntimeShutdown(runtime, hostLifetime); hostLifetime.RegisterObject(runtimeShutdown); // Register our global threadabort enricher for logging - var threadAbortEnricher = app.ApplicationServices.GetRequiredService(); + ThreadAbortExceptionEnricher threadAbortEnricher = app.ApplicationServices.GetRequiredService(); LogContext.Push(threadAbortEnricher); // NOTE: We are not in a using clause because we are not removing it, it is on the global context StaticApplicationLogging.Initialize(app.ApplicationServices.GetRequiredService()); @@ -64,12 +114,16 @@ namespace Umbraco.Extensions /// /// Enables middlewares required to run Umbraco /// - /// - /// - // TODO: Could be internal or part of another call - this is a required system so should't be 'opt-in' + /// + /// Must occur before UseRouting + /// public static IApplicationBuilder UseUmbracoRouting(this IApplicationBuilder app) { - if (app == null) throw new ArgumentNullException(nameof(app)); + // TODO: This method could be internal or part of another call - this is a required system so should't be 'opt-in' + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } if (!app.UmbracoCanBoot()) { @@ -79,11 +133,6 @@ namespace Umbraco.Extensions { app.UseMiddleware(); app.UseMiddleware(); - - // TODO: Both of these need to be done before any endpoints but after UmbracoRequestMiddleware - // because they rely on an UmbracoContext. But should they be here? - app.UseAuthentication(); - app.UseAuthorization(); } return app; @@ -92,11 +141,12 @@ namespace Umbraco.Extensions /// /// Adds request based serilog enrichers to the LogContext for each request /// - /// - /// public static IApplicationBuilder UseUmbracoRequestLogging(this IApplicationBuilder app) { - if (app == null) throw new ArgumentNullException(nameof(app)); + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } if (!app.UmbracoCanBoot()) return app; @@ -108,13 +158,17 @@ namespace Umbraco.Extensions /// /// Enables runtime minification for Umbraco /// - /// - /// public static IApplicationBuilder UseUmbracoRuntimeMinification(this IApplicationBuilder app) { - if (app == null) throw new ArgumentNullException(nameof(app)); + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } - if (!app.UmbracoCanBoot()) return app; + if (!app.UmbracoCanBoot()) + { + return app; + } app.UseSmidge(); app.UseSmidgeNuglify(); diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoInstallApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoInstallApplicationBuilderExtensions.cs index 3350af756e..ac5d787911 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoInstallApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoInstallApplicationBuilderExtensions.cs @@ -1,30 +1,31 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Umbraco.Web.Common.Install; namespace Umbraco.Extensions { + /// + /// extensions for Umbraco installer + /// public static class UmbracoInstallApplicationBuilderExtensions { /// /// Enables the Umbraco installer /// - /// - /// public static IApplicationBuilder UseUmbracoInstaller(this IApplicationBuilder app) { - if (!app.UmbracoCanBoot()) return app; + if (!app.UmbracoCanBoot()) + { + return app; + } app.UseEndpoints(endpoints => { - var installerRoutes = app.ApplicationServices.GetRequiredService(); + InstallAreaRoutes installerRoutes = app.ApplicationServices.GetRequiredService(); installerRoutes.CreateRoutes(endpoints); }); return app; } - - } - } diff --git a/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs index 360396fe04..c53c367689 100644 --- a/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Umbraco.Web.Common.Constants; using Umbraco.Web.Common.Controllers; -using Umbraco.Web.Mvc; namespace Umbraco.Web.Common.Filters { @@ -35,13 +34,18 @@ namespace Umbraco.Web.Common.Filters /// this ensures that any calls to GetPropertyValue with regards to RTE or Grid editors can still /// render any PartialViewMacro with a form and maintain ModelState /// - /// public override void OnActionExecuting(ActionExecutingContext context) { - if (!(context.Controller is Controller controller)) return; + if (!(context.Controller is Controller controller)) + { + return; + } - //ignore anything that is not IRenderController - if (!(controller is IRenderController)) return; + // ignore anything that is not IRenderController + if (!(controller is IRenderController)) + { + return; + } SetViewContext(context, controller); } @@ -54,10 +58,16 @@ namespace Umbraco.Web.Common.Filters /// The filter context. public override void OnResultExecuting(ResultExecutingContext context) { - if (!(context.Controller is Controller controller)) return; + if (!(context.Controller is Controller controller)) + { + return; + } - //ignore anything that is not IRenderController - if (!(controller is RenderController)) return; + // ignore anything that is not IRenderController + if (!(controller is IRenderController)) + { + return; + } SetViewContext(context, controller); } @@ -72,16 +82,13 @@ namespace Umbraco.Web.Common.Filters new StringWriter(), new HtmlHelperOptions()); - //set the special data token + // set the special data token context.RouteData.DataTokens[ViewConstants.DataTokenCurrentViewContext] = viewCtx; } private class DummyView : IView { - public Task RenderAsync(ViewContext context) - { - return Task.CompletedTask; - } + public Task RenderAsync(ViewContext context) => Task.CompletedTask; public string Path { get; } } diff --git a/src/Umbraco.Web.Common/Filters/PreRenderViewActionFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/PreRenderViewActionFilterAttribute.cs deleted file mode 100644 index 2ba58a8fd7..0000000000 --- a/src/Umbraco.Web.Common/Filters/PreRenderViewActionFilterAttribute.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Umbraco.Web.Common.Events; - -namespace Umbraco.Web.Common.Filters -{ - public class PreRenderViewActionFilterAttribute : ActionFilterAttribute - { - public override void OnActionExecuted(ActionExecutedContext context) - { - if (!(context.Controller is Controller umbController) || !(context.Result is ViewResult result)) - { - return; - } - - var model = result.Model; - if (model == null) - { - return; - } - - var args = new ActionExecutedEventArgs(umbController, model); - OnActionExecuted(args); - - if (args.Model != model) - { - result.ViewData.Model = args.Model; - } - - base.OnActionExecuted(context); - } - - - public static event EventHandler ActionExecuted; - - private static void OnActionExecuted(ActionExecutedEventArgs e) - { - var handler = ActionExecuted; - handler?.Invoke(null, e); - } - } -} diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs index e274e479b7..6b5d305a64 100644 --- a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs @@ -1,15 +1,13 @@ -using System; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; -using Umbraco.Web.Common.Lifetime; using Umbraco.Core; -using Umbraco.Core.Logging; -using System.Threading; using Umbraco.Core.Cache; -using System.Collections.Generic; -using Umbraco.Core.Security; +using Umbraco.Core.Logging; +using Umbraco.Web.Common.Lifetime; namespace Umbraco.Web.Common.Middleware { @@ -28,6 +26,9 @@ namespace Umbraco.Web.Common.Middleware private readonly IRequestCache _requestCache; private readonly IBackOfficeSecurityFactory _backofficeSecurityFactory; + /// + /// Initializes a new instance of the class. + /// public UmbracoRequestMiddleware( ILogger logger, IUmbracoRequestLifetimeManager umbracoRequestLifetimeManager, @@ -42,6 +43,7 @@ namespace Umbraco.Web.Common.Middleware _backofficeSecurityFactory = backofficeSecurityFactory; } + /// public async Task InvokeAsync(HttpContext context, RequestDelegate next) { var requestUri = new Uri(context.Request.GetEncodedUrl(), UriKind.RelativeOrAbsolute); @@ -52,16 +54,16 @@ namespace Umbraco.Web.Common.Middleware await next(context); return; } - _backofficeSecurityFactory.EnsureBackOfficeSecurity(); // Needs to be before UmbracoContext - var umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); + _backofficeSecurityFactory.EnsureBackOfficeSecurity(); // Needs to be before UmbracoContext + UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); try { if (umbracoContextReference.UmbracoContext.IsFrontEndUmbracoRequest) { - LogHttpRequest.TryGetCurrentHttpRequestId(out var httpRequestId, _requestCache); - _logger.LogTrace("Begin request [{HttpRequestId}]: {RequestUrl}", httpRequestId, requestUri); + LogHttpRequest.TryGetCurrentHttpRequestId(out Guid httpRequestId, _requestCache); + _logger.LogTrace("Begin request [{HttpRequestId}]: {RequestUrl}", httpRequestId, requestUri); } try diff --git a/src/Umbraco.Web.Common/Routing/IAreaRoutes.cs b/src/Umbraco.Web.Common/Routing/IAreaRoutes.cs index b561abc4dd..b01f703016 100644 --- a/src/Umbraco.Web.Common/Routing/IAreaRoutes.cs +++ b/src/Umbraco.Web.Common/Routing/IAreaRoutes.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing; namespace Umbraco.Web.Common.Routing { @@ -11,6 +11,10 @@ namespace Umbraco.Web.Common.Routing // on individual ext methods. This would reduce the amount of code in Startup, but could also mean there's less control over startup // if someone wanted that. Maybe we can just have both. + /// + /// Create routes for an area + /// + /// The endpoint route builder void CreateRoutes(IEndpointRouteBuilder endpoints); } } diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index d496aadfd3..6b57c8af9d 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -54,6 +54,7 @@ namespace Umbraco.Web.UI.NetCore } app.UseUmbraco(); + app.UseUmbracoBackOffice(); app.UseUmbracoWebsite(); } } diff --git a/src/Umbraco.Web.Website/Controllers/IUmbracoRenderingDefaults.cs b/src/Umbraco.Web.Website/Controllers/IUmbracoRenderingDefaults.cs new file mode 100644 index 0000000000..507b8c4a04 --- /dev/null +++ b/src/Umbraco.Web.Website/Controllers/IUmbracoRenderingDefaults.cs @@ -0,0 +1,15 @@ +using System; + +namespace Umbraco.Web.Website.Controllers +{ + /// + /// The defaults used for rendering Umbraco front-end pages + /// + public interface IUmbracoRenderingDefaults + { + /// + /// Gets the default umbraco render controller type + /// + Type DefaultControllerType { get; } + } +} diff --git a/src/Umbraco.Web.Website/Controllers/RenderMvcController.cs b/src/Umbraco.Web.Website/Controllers/RenderController.cs similarity index 78% rename from src/Umbraco.Web.Website/Controllers/RenderMvcController.cs rename to src/Umbraco.Web.Website/Controllers/RenderController.cs index 62ffb010ea..071560d860 100644 --- a/src/Umbraco.Web.Website/Controllers/RenderMvcController.cs +++ b/src/Umbraco.Web.Website/Controllers/RenderController.cs @@ -3,25 +3,28 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.Extensions.Logging; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Filters; using Umbraco.Web.Models; using Umbraco.Web.Routing; -namespace Umbraco.Web.Mvc +namespace Umbraco.Web.Website.Controllers { /// /// Represents the default front-end rendering controller. /// - [PreRenderViewActionFilter] [TypeFilter(typeof(ModelBindingExceptionFilter))] - public class RenderMvcController : UmbracoController, IRenderMvcController + public class RenderController : UmbracoController, IRenderController { private IPublishedRequest _publishedRequest; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ICompositeViewEngine _compositeViewEngine; - public RenderMvcController(ILogger logger, ICompositeViewEngine compositeViewEngine) + /// + /// Initializes a new instance of the class. + /// + public RenderController(ILogger logger, ICompositeViewEngine compositeViewEngine) { _logger = logger; _compositeViewEngine = compositeViewEngine; @@ -40,11 +43,15 @@ namespace Umbraco.Web.Mvc get { if (_publishedRequest != null) + { return _publishedRequest; + } + if (RouteData.DataTokens.ContainsKey(Core.Constants.Web.PublishedDocumentRequestDataToken) == false) { throw new InvalidOperationException("DataTokens must contain an 'umbraco-doc-request' key with a PublishedRequest object"); } + _publishedRequest = (IPublishedRequest)RouteData.DataTokens[Core.Constants.Web.PublishedDocumentRequestDataToken]; return _publishedRequest; } @@ -56,8 +63,11 @@ namespace Umbraco.Web.Mvc /// The view name. protected bool EnsurePhsyicalViewExists(string template) { - var result = _compositeViewEngine.FindView(ControllerContext, template, false); - if (result.View != null) return true; + ViewEngineResult result = _compositeViewEngine.FindView(ControllerContext, template, false); + if (result.View != null) + { + return true; + } _logger.LogWarning("No physical template file was found for template {Template}", template); return false; @@ -74,19 +84,17 @@ namespace Umbraco.Web.Mvc { var template = ControllerContext.RouteData.Values["action"].ToString(); if (EnsurePhsyicalViewExists(template) == false) + { throw new InvalidOperationException("No physical template file was found for template " + template); + } + return View(template, model); } /// /// The default action to render the front-end view. /// - /// - /// [RenderIndexActionSelector] - public virtual IActionResult Index(ContentModel model) - { - return CurrentTemplate(model); - } + public virtual IActionResult Index(ContentModel model) => CurrentTemplate(model); } } diff --git a/src/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttribute.cs b/src/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttribute.cs index f1ea65e983..0027132c23 100644 --- a/src/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttribute.cs +++ b/src/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -namespace Umbraco.Web.Mvc +namespace Umbraco.Web.Website.Controllers { /// /// A custom ActionMethodSelector which will ensure that the RenderMvcController.Index(ContentModel model) action will be executed @@ -35,17 +35,18 @@ namespace Umbraco.Web.Mvc var baseType = controllerAction.ControllerTypeInfo.BaseType; //It's the same type, so this must be the Index action to use - if (currType == baseType) return true; + if (currType == baseType) + return true; - var actions = _controllerActionsCache.GetOrAdd(currType, type => - { - var actionDescriptors = routeContext.HttpContext.RequestServices - .GetRequiredService().ActionDescriptors.Items - .Where(x=>x is ControllerActionDescriptor).Cast() - .Where(x => x.ControllerTypeInfo == controllerAction.ControllerTypeInfo); + var actions = _controllerActionsCache.GetOrAdd(currType, type => + { + var actionDescriptors = routeContext.HttpContext.RequestServices + .GetRequiredService().ActionDescriptors.Items + .Where(x => x is ControllerActionDescriptor).Cast() + .Where(x => x.ControllerTypeInfo == controllerAction.ControllerTypeInfo); - return actionDescriptors; - }); + return actionDescriptors; + }); //If there are more than one Index action for this controller, then // this base class one should not be matched diff --git a/src/Umbraco.Web.Website/Controllers/SurfaceController.cs b/src/Umbraco.Web.Website/Controllers/SurfaceController.cs index 4e0517754c..1d3e4c5626 100644 --- a/src/Umbraco.Web.Website/Controllers/SurfaceController.cs +++ b/src/Umbraco.Web.Website/Controllers/SurfaceController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Specialized; using Microsoft.AspNetCore.Http; using Umbraco.Core; @@ -10,6 +10,7 @@ using Umbraco.Core.Services; using Umbraco.Web.Common.Controllers; using Umbraco.Web.Routing; using Umbraco.Web.Website.ActionResults; +using Umbraco.Web.Website.Routing; namespace Umbraco.Web.Website.Controllers { diff --git a/src/Umbraco.Web.Website/Controllers/UmbracoRenderingDefaults.cs b/src/Umbraco.Web.Website/Controllers/UmbracoRenderingDefaults.cs new file mode 100644 index 0000000000..669e1835d4 --- /dev/null +++ b/src/Umbraco.Web.Website/Controllers/UmbracoRenderingDefaults.cs @@ -0,0 +1,13 @@ +using System; + +namespace Umbraco.Web.Website.Controllers +{ + /// + /// The defaults used for rendering Umbraco front-end pages + /// + public class UmbracoRenderingDefaults : IUmbracoRenderingDefaults + { + /// + public Type DefaultControllerType => typeof(RenderController); + } +} diff --git a/src/Umbraco.Web.Website/Extensions/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/Extensions/UmbracoBuilderExtensions.cs index 72d12809e2..cbfa0c659e 100644 --- a/src/Umbraco.Web.Website/Extensions/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/UmbracoBuilderExtensions.cs @@ -1,14 +1,21 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Core.DependencyInjection; -using Umbraco.Core.DependencyInjection; +using Umbraco.Web.Website.Controllers; +using Umbraco.Web.Website.Routing; using Umbraco.Web.Website.ViewEngines; namespace Umbraco.Extensions { + /// + /// extensions for umbraco front-end website + /// public static class UmbracoBuilderExtensions { + /// + /// Add services for the umbraco front-end website + /// public static IUmbracoBuilder AddUmbracoWebsite(this IUmbracoBuilder builder) { // Set the render & plugin view engines (Super complicated, but this allows us to use the IServiceCollection @@ -21,12 +28,14 @@ namespace Umbraco.Extensions // Wraps all existing view engines in a ProfilerViewEngine builder.Services.AddTransient, ProfilingViewEngineWrapperMvcViewOptionsSetup>(); - //TODO figure out if we need more to work on load balanced setups + // TODO figure out if we need more to work on load balanced setups builder.Services.AddDataProtection(); + builder.Services.AddScoped(); + builder.Services.AddSingleton(); + return builder; } - } } diff --git a/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs b/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs index ef99d67373..32d84088c1 100644 --- a/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs @@ -6,28 +6,41 @@ using Umbraco.Web.Website.Routing; namespace Umbraco.Extensions { + /// + /// extensions for the umbraco front-end website + /// public static class UmbracoWebsiteApplicationBuilderExtensions { + /// + /// Sets up services and routes for the front-end umbraco website + /// public static IApplicationBuilder UseUmbracoWebsite(this IApplicationBuilder app) { - if (app == null) throw new ArgumentNullException(nameof(app)); + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } - if (!app.UmbracoCanBoot()) return app; + if (!app.UmbracoCanBoot()) + { + return app; + } - // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. - // TODO: Since we are dependent on these we need to register them but what happens when we call this multiple times since we are dependent on this for UseUmbracoBackOffice too? - app.UseImageSharp(); - app.UseStaticFiles(); - app.UseUmbracoNoContentPage(); + app.UseUmbracoRoutes(); return app; } - public static IApplicationBuilder UseUmbracoNoContentPage(this IApplicationBuilder app) + /// + /// Sets up routes for the umbraco front-end + /// + public static IApplicationBuilder UseUmbracoRoutes(this IApplicationBuilder app) { app.UseEndpoints(endpoints => { - var noContentRoutes = app.ApplicationServices.GetRequiredService(); + endpoints.MapDynamicControllerRoute("/{**slug}"); + + NoContentRoutes noContentRoutes = app.ApplicationServices.GetRequiredService(); noContentRoutes.CreateRoutes(endpoints); }); diff --git a/src/Umbraco.Web.Website/Routing/NoContentRoutes.cs b/src/Umbraco.Web.Website/Routing/NoContentRoutes.cs index 58885bcd96..f2f2e8dfe3 100644 --- a/src/Umbraco.Web.Website/Routing/NoContentRoutes.cs +++ b/src/Umbraco.Web.Website/Routing/NoContentRoutes.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; using Umbraco.Core; @@ -17,6 +17,9 @@ namespace Umbraco.Web.Website.Routing private readonly IRuntimeState _runtimeState; private readonly string _umbracoPathSegment; + /// + /// Initializes a new instance of the class. + /// public NoContentRoutes( IOptions globalSettings, IHostingEnvironment hostingEnvironment, @@ -26,6 +29,7 @@ namespace Umbraco.Web.Website.Routing _umbracoPathSegment = globalSettings.Value.GetUmbracoMvcArea(hostingEnvironment); } + /// public void CreateRoutes(IEndpointRouteBuilder endpoints) { switch (_runtimeState.Level) @@ -35,13 +39,16 @@ namespace Umbraco.Web.Website.Routing case RuntimeLevel.Upgrade: break; case RuntimeLevel.Run: + + // TODO: I don't really think this is working AFAIK the code has just been migrated but it's not really enabled + // yet. Our route handler needs to be aware that there is no content and redirect there. Though, this could all be + // managed directly in UmbracoRouteValueTransformer. Else it could actually do a 'redirect' but that would need to be + // an internal rewrite. endpoints.MapControllerRoute( - // named consistently - Constants.Web.NoContentRouteName, + Constants.Web.NoContentRouteName, // named consistently _umbracoPathSegment + "/UmbNoContent", - new { controller = "RenderNoContent", action = "Index" } - ); - break; + new { controller = "RenderNoContent", action = "Index" }); + break; case RuntimeLevel.BootFailed: case RuntimeLevel.Unknown: case RuntimeLevel.Boot: diff --git a/src/Umbraco.Web.Website/RouteDefinition.cs b/src/Umbraco.Web.Website/Routing/RouteDefinition.cs similarity index 94% rename from src/Umbraco.Web.Website/RouteDefinition.cs rename to src/Umbraco.Web.Website/Routing/RouteDefinition.cs index 02eab6ae77..47206bd0c3 100644 --- a/src/Umbraco.Web.Website/RouteDefinition.cs +++ b/src/Umbraco.Web.Website/Routing/RouteDefinition.cs @@ -1,7 +1,7 @@ -using System; +using System; using Umbraco.Web.Routing; -namespace Umbraco.Web.Website +namespace Umbraco.Web.Website.Routing { /// /// Represents the data required to route to a specific controller/action during an Umbraco request diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs new file mode 100644 index 0000000000..a6582f03ae --- /dev/null +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -0,0 +1,230 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Strings; +using Umbraco.Extensions; +using Umbraco.Web.Common.Controllers; +using Umbraco.Web.Common.Middleware; +using Umbraco.Web.Models; +using Umbraco.Web.Routing; +using Umbraco.Web.Website.Controllers; + +namespace Umbraco.Web.Website.Routing +{ + /// + /// The route value transformer for Umbraco front-end routes + /// + public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer + { + private readonly ILogger _logger; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IUmbracoRenderingDefaults _renderingDefaults; + private readonly IShortStringHelper _shortStringHelper; + private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; + private readonly IPublishedRouter _publishedRouter; + + /// + /// Initializes a new instance of the class. + /// + public UmbracoRouteValueTransformer( + ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoRenderingDefaults renderingDefaults, + IShortStringHelper shortStringHelper, + IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, + IPublishedRouter publishedRouter) + { + _logger = logger; + _umbracoContextAccessor = umbracoContextAccessor; + _renderingDefaults = renderingDefaults; + _shortStringHelper = shortStringHelper; + _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; + _publishedRouter = publishedRouter; + } + + /// + public override async ValueTask TransformAsync(HttpContext httpContext, RouteValueDictionary values) + { + if (_umbracoContextAccessor.UmbracoContext == null) + { + throw new InvalidOperationException($"There is no current UmbracoContext, it must be initialized before the {nameof(UmbracoRouteValueTransformer)} executes, ensure that {nameof(UmbracoRequestMiddleware)} is registered prior to 'UseRouting'"); + } + + bool routed = RouteRequest(_umbracoContextAccessor.UmbracoContext); + if (!routed) + { + // TODO: Deal with it not being routable, perhaps this should be an enum result? + } + + IPublishedRequest request = _umbracoContextAccessor.UmbracoContext.PublishedRequest; // This cannot be null here + + SetupRouteDataForRequest( + new ContentModel(request.PublishedContent), + request, + values); + + RouteDefinition routeDef = GetUmbracoRouteDefinition(httpContext, values, request); + values["controller"] = routeDef.ControllerName; + if (string.IsNullOrWhiteSpace(routeDef.ActionName) == false) + { + values["action"] = routeDef.ActionName; + } + + return await Task.FromResult(values); + } + + /// + /// Ensures that all of the correct DataTokens are added to the route values which are all required for rendering front-end umbraco views + /// + private void SetupRouteDataForRequest(ContentModel contentModel, IPublishedRequest frequest, RouteValueDictionary values) + { + // put essential data into the data tokens, the 'umbraco' key is required to be there for the view engine + + // required for the ContentModelBinder and view engine. + // TODO: Are we sure, seems strange to need this in netcore + values.TryAdd(Constants.Web.UmbracoDataToken, contentModel); + + // required for RenderMvcController + // TODO: Are we sure, seems strange to need this in netcore + values.TryAdd(Constants.Web.PublishedDocumentRequestDataToken, frequest); + + // required for UmbracoViewPage + // TODO: Are we sure, seems strange to need this in netcore + values.TryAdd(Constants.Web.UmbracoContextDataToken, _umbracoContextAccessor.UmbracoContext); + } + + /// + /// Returns a object based on the current content request + /// + private RouteDefinition GetUmbracoRouteDefinition(HttpContext httpContext, RouteValueDictionary values, IPublishedRequest request) + { + if (httpContext is null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (values is null) + { + throw new ArgumentNullException(nameof(values)); + } + + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + Type defaultControllerType = _renderingDefaults.DefaultControllerType; + var defaultControllerName = ControllerExtensions.GetControllerName(defaultControllerType); + + // creates the default route definition which maps to the 'UmbracoController' controller + var def = new RouteDefinition + { + ControllerName = defaultControllerName, + ControllerType = defaultControllerType, + PublishedRequest = request, + + // ActionName = ((Route)requestContext.RouteData.Route).Defaults["action"].ToString(), + ActionName = "Index", + HasHijackedRoute = false + }; + + // check that a template is defined), if it doesn't and there is a hijacked route it will just route + // to the index Action + if (request.HasTemplate) + { + // the template Alias should always be already saved with a safe name. + // if there are hyphens in the name and there is a hijacked route, then the Action will need to be attributed + // with the action name attribute. + var templateName = request.TemplateAlias.Split('.')[0].ToSafeAlias(_shortStringHelper); + def.ActionName = templateName; + } + + // check if there's a custom controller assigned, base on the document type alias. + Type controllerType = FindControllerType(request.PublishedContent.ContentType.Alias); + + // check if that controller exists + if (controllerType != null) + { + // ensure the controller is of type IRenderController and ControllerBase + if (TypeHelper.IsTypeAssignableFrom(controllerType) + && TypeHelper.IsTypeAssignableFrom(controllerType)) + { + // set the controller and name to the custom one + def.ControllerType = controllerType; + def.ControllerName = ControllerExtensions.GetControllerName(controllerType); + if (def.ControllerName != defaultControllerName) + { + def.HasHijackedRoute = true; + } + } + else + { + _logger.LogWarning( + "The current Document Type {ContentTypeAlias} matches a locally declared controller of type {ControllerName}. Custom Controllers for Umbraco routing must implement '{UmbracoRenderController}' and inherit from '{UmbracoControllerBase}'.", + request.PublishedContent.ContentType.Alias, + controllerType.FullName, + typeof(IRenderController).FullName, + typeof(ControllerBase).FullName); + + // we cannot route to this custom controller since it is not of the correct type so we'll continue with the defaults + // that have already been set above. + } + } + + // store the route definition + values.TryAdd(Constants.Web.UmbracoRouteDefinitionDataToken, def); + + return def; + } + + private Type FindControllerType(string controllerName) + { + ControllerActionDescriptor descriptor = _actionDescriptorCollectionProvider.ActionDescriptors.Items + .Cast() + .First(x => + x.ControllerName.Equals(controllerName)); + + return descriptor?.ControllerTypeInfo; + } + + private bool RouteRequest(IUmbracoContext umbracoContext) + { + // TODO: I suspect one day this will be async + + // ok, process + + // note: requestModule.UmbracoRewrite also did some stripping of &umbPage + // from the querystring... that was in v3.x to fix some issues with pre-forms + // auth. Paul Sterling confirmed in Jan. 2013 that we can get rid of it. + + // instantiate, prepare and process the published content request + // important to use CleanedUmbracoUrl - lowercase path-only version of the current url + IPublishedRequest request = _publishedRouter.CreateRequest(umbracoContext); + umbracoContext.PublishedRequest = request; // TODO: This is ugly + bool prepared = _publishedRouter.PrepareRequest(request); + return prepared && request.HasPublishedContent; + + // // HandleHttpResponseStatus returns a value indicating that the request should + // // not be processed any further, eg because it has been redirect. then, exit. + // if (UmbracoModule.HandleHttpResponseStatus(httpContext, request, _logger)) + // return; + // if (!request.HasPublishedContent == false) + // { + // // httpContext.RemapHandler(new PublishedContentNotFoundHandler()); + // } + // else + // { + // // RewriteToUmbracoHandler(httpContext, request); + // } + } + } +} diff --git a/src/Umbraco.Web/Mvc/RenderControllerFactory.cs b/src/Umbraco.Web/Mvc/RenderControllerFactory.cs deleted file mode 100644 index 091ace4d98..0000000000 --- a/src/Umbraco.Web/Mvc/RenderControllerFactory.cs +++ /dev/null @@ -1,45 +0,0 @@ -// using System.Web.Mvc; -// using System.Web.Routing; -// -// namespace Umbraco.Web.Mvc -// { -// /// -// /// A controller factory for the render pipeline of Umbraco. This controller factory tries to create a controller with the supplied -// /// name, and falls back to UmbracoController if none was found. -// /// -// /// -// public class RenderControllerFactory : UmbracoControllerFactory -// { -// /// -// /// Determines whether this instance can handle the specified request. -// /// -// /// The request. -// /// true if this instance can handle the specified request; otherwise, false. -// /// -// public override bool CanHandle(RequestContext request) -// { -// var dataToken = request.RouteData.DataTokens["area"]; -// return dataToken == null || string.IsNullOrWhiteSpace(dataToken.ToString()); -// } -// -// /// -// /// Creates the controller -// /// -// /// -// /// -// /// -// /// -// /// We always set the correct ActionInvoker on our custom created controller, this is very important for route hijacking! -// /// -// public override IController CreateController(RequestContext requestContext, string controllerName) -// { -// var instance = base.CreateController(requestContext, controllerName); -// if (instance is Controller controllerInstance) -// { -// //set the action invoker! -// controllerInstance.ActionInvoker = new RenderActionInvoker(); -// } -// return instance; -// } -// } -// } diff --git a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs index 163364602d..19e1b79c89 100644 --- a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs @@ -338,10 +338,10 @@ namespace Umbraco.Web.Mvc } - //Here we need to check if there is no hijacked route and no template assigned, - //if this is the case we want to return a blank page, but we'll leave that up to the NoTemplateHandler. - //We also check if templates have been disabled since if they are then we're allowed to render even though there's no template, - //for example for json rendering in headless. + // Here we need to check if there is no hijacked route and no template assigned, + // if this is the case we want to return a blank page, but we'll leave that up to the NoTemplateHandler. + // We also check if templates have been disabled since if they are then we're allowed to render even though there's no template, + // for example for json rendering in headless. if ((request.HasTemplate == false && Features.Disabled.DisableTemplates == false) && routeDef.HasHijackedRoute == false) { @@ -370,7 +370,7 @@ namespace Umbraco.Web.Mvc routeDef = GetUmbracoRouteDefinition(requestContext, request); } - //no post values, just route to the controller/action required (local) + // no post values, just route to the controller/action required (local) requestContext.RouteData.Values["controller"] = routeDef.ControllerName; if (string.IsNullOrWhiteSpace(routeDef.ActionName) == false) diff --git a/src/Umbraco.Web/Mvc/RouteDefinition.cs b/src/Umbraco.Web/Mvc/RouteDefinition.cs index 45e759fd66..2977c49cb5 100644 --- a/src/Umbraco.Web/Mvc/RouteDefinition.cs +++ b/src/Umbraco.Web/Mvc/RouteDefinition.cs @@ -1,13 +1,9 @@ -using System; -using System.Web.Mvc; +using System; using Umbraco.Web.Routing; namespace Umbraco.Web.Mvc { - /// - /// Represents the data required to route to a specific controller/action during an Umbraco request - /// - /// Migrated already to .Net Core + // TODO: Migrated already to .Net Core public class RouteDefinition { public string ControllerName { get; set; } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index ad7dd86730..8867971657 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -241,7 +241,6 @@ - diff --git a/src/Umbraco.Web/UmbracoInjectedModule.cs b/src/Umbraco.Web/UmbracoInjectedModule.cs index e9a1cc1436..5c7468ce95 100644 --- a/src/Umbraco.Web/UmbracoInjectedModule.cs +++ b/src/Umbraco.Web/UmbracoInjectedModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Web; using System.Web.Routing; using Microsoft.Extensions.Logging; @@ -157,9 +157,6 @@ namespace Umbraco.Web /// /// Checks the current request and ensures that it is routable based on the structure of the request and URI /// - /// - /// - /// internal Attempt EnsureUmbracoRoutablePage(IUmbracoContext context, HttpContextBase httpContext) { var uri = context.OriginalRequestUrl; @@ -186,8 +183,8 @@ namespace Umbraco.Web return Attempt.If(reason == EnsureRoutableOutcome.IsRoutable, reason); } - - + // TODO: Where should this execute in netcore? This will have to be a middleware + // executing before UseRouting so that it is done before any endpoint routing takes place. private bool EnsureRuntime(HttpContextBase httpContext, Uri uri) { var level = _runtime.Level; From 4b85f8eb20525884c11b8684b81ce7a850884409 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 9 Dec 2020 22:43:49 +1100 Subject: [PATCH 02/14] Big refactor or PublishedSnapshotService to split up so that there's a service and repository responsible for the data querying and persistence --- .../IPublishedSnapshotService.cs | 30 +- .../PublishedSnapshotServiceBase.cs | 43 +- .../Persistence/Dtos/ContentNuDto.cs | 2 +- .../Implement/ContentRepositoryBase.cs | 73 +- .../Implement/EntityRepository.cs | 35 +- ...fTIdTEntity.cs => EntityRepositoryBase.cs} | 187 ++-- .../Implement/NPocoRepositoryBase.cs | 61 +- .../Repositories/Implement/RepositoryBase.cs | 81 ++ .../ContentTypeServiceBaseOfTItemTService.cs | 23 +- .../Compose/ModelsBuilderComposer.cs | 3 +- .../ContentCache.cs | 2 +- .../DataSource/ContentNestedData.cs | 6 +- .../DataSource/DatabaseDataSource.cs | 325 ------ .../DataSource/IDataSource.cs | 77 -- .../DataSource/PropertyData.cs | 4 +- .../NuCacheComponent.cs | 18 - .../NuCacheComposer.cs | 26 +- .../Persistence/INuCacheContentRepository.cs | 46 + .../Persistence/INuCacheContentService.cs | 96 ++ .../Persistence/NuCacheContentRepository.cs | 735 ++++++++++++++ .../Persistence/NuCacheContentService.cs | 107 ++ .../PublishedSnapshotService.cs | 944 ++++-------------- .../PublishedSnapshotServiceEventHandler.cs | 187 ++++ .../Repositories/DocumentRepositoryTest.cs | 4 +- .../Repositories/EntityRepositoryTest.cs | 5 +- .../Repositories/MediaRepositoryTest.cs | 4 +- .../Repositories/RelationRepositoryTest.cs | 4 +- .../Repositories/TemplateRepositoryTest.cs | 4 +- .../Services/EntityServiceTests.cs | 9 +- .../PublishedContent/NuCacheChildrenTests.cs | 17 +- .../PublishedContent/NuCacheTests.cs | 17 +- .../Scoping/ScopedNuCacheTests.cs | 15 +- .../Testing/Objects/TestDataSource.cs | 50 +- .../ApplicationBuilderExtensions.cs | 12 + .../Middleware/UmbracoRequestMiddleware.cs | 2 +- .../Umbraco.Web.Common.csproj | 1 + .../Routing/UmbracoRouteValueTransformer.cs | 2 +- 37 files changed, 1766 insertions(+), 1491 deletions(-) rename src/Umbraco.Infrastructure/Persistence/Repositories/Implement/{RepositoryBaseOfTIdTEntity.cs => EntityRepositoryBase.cs} (63%) create mode 100644 src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/DatabaseDataSource.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/DataSource/IDataSource.cs delete mode 100644 src/Umbraco.PublishedCache.NuCache/NuCacheComponent.cs create mode 100644 src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentRepository.cs create mode 100644 src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentService.cs create mode 100644 src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs create mode 100644 src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentService.cs create mode 100644 src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs index 68b2367ce0..73ce858b52 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs @@ -11,8 +11,6 @@ namespace Umbraco.Web.PublishedCache /// public interface IPublishedSnapshotService : IDisposable { - #region PublishedSnapshot - /* Various places (such as Node) want to access the XML content, today as an XmlDocument * but to migrate to a new cache, they're migrating to an XPathNavigator. Still, they need * to find out how to get that navigator. @@ -25,6 +23,8 @@ namespace Umbraco.Web.PublishedCache * */ + void LoadCachesOnStartup(); + /// /// Creates a published snapshot. /// @@ -47,20 +47,26 @@ namespace Umbraco.Web.PublishedCache /// A value indicating whether the published snapshot has the proper environment to run. bool EnsureEnvironment(out IEnumerable errors); - #endregion - #region Rebuild /// /// Rebuilds internal caches (but does not reload). /// + /// The operation batch size to process the items + /// If not null will process content for the matching content types, if empty will process all content + /// If not null will process content for the matching media types, if empty will process all media + /// If not null will process content for the matching members types, if empty will process all members /// /// Forces the snapshot service to rebuild its internal caches. For instance, some caches /// may rely on a database table to store pre-serialized version of documents. /// This does *not* reload the caches. Caches need to be reloaded, for instance via /// RefreshAllPublishedSnapshot method. /// - void Rebuild(); + void Rebuild( + int groupSize = 5000, + IReadOnlyCollection contentTypeIds = null, + IReadOnlyCollection mediaTypeIds = null, + IReadOnlyCollection memberTypeIds = null); #endregion @@ -84,11 +90,11 @@ namespace Umbraco.Web.PublishedCache /// A preview token. /// /// Tells the caches that they should prepare any data that they would be keeping - /// in order to provide preview to a give user. In the Xml cache this means creating the Xml + /// in order to provide preview to a given user. In the Xml cache this means creating the Xml /// file, though other caches may do things differently. /// Does not handle the preview token storage (cookie, etc) that must be handled separately. /// - string EnterPreview(IUser user, int contentId); + string EnterPreview(IUser user, int contentId); // TODO: Remove this, it is not needed and is legacy from the XML cache /// /// Refreshes preview for a specified content. @@ -98,7 +104,7 @@ namespace Umbraco.Web.PublishedCache /// Tells the caches that they should update any data that they would be keeping /// in order to provide preview to a given user. In the Xml cache this means updating the Xml /// file, though other caches may do things differently. - void RefreshPreview(string previewToken, int contentId); + void RefreshPreview(string previewToken, int contentId); // TODO: Remove this, it is not needed and is legacy from the XML cache /// /// Exits preview for a specified preview token. @@ -110,7 +116,7 @@ namespace Umbraco.Web.PublishedCache /// though other caches may do things differently. /// Does not handle the preview token storage (cookie, etc) that must be handled separately. /// - void ExitPreview(string previewToken); + void ExitPreview(string previewToken); // TODO: Remove this, it is not needed and is legacy from the XML cache #endregion @@ -162,14 +168,12 @@ namespace Umbraco.Web.PublishedCache #endregion - #region Status - + // TODO: This is weird, why is this is this a thing? Maybe IPublishedSnapshotStatus? string GetStatus(); + // TODO: This is weird, why is this is this a thing? Maybe IPublishedSnapshotStatus? string StatusUrl { get; } - #endregion - void Collect(); } } diff --git a/src/Umbraco.Core/PublishedCache/PublishedSnapshotServiceBase.cs b/src/Umbraco.Core/PublishedCache/PublishedSnapshotServiceBase.cs index 9c71bdc04b..6a8324cc27 100644 --- a/src/Umbraco.Core/PublishedCache/PublishedSnapshotServiceBase.cs +++ b/src/Umbraco.Core/PublishedCache/PublishedSnapshotServiceBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.PublishedContent; @@ -8,44 +8,83 @@ namespace Umbraco.Web.PublishedCache { public abstract class PublishedSnapshotServiceBase : IPublishedSnapshotService { + /// + /// Initializes a new instance of the class. + /// protected PublishedSnapshotServiceBase(IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor) { PublishedSnapshotAccessor = publishedSnapshotAccessor; VariationContextAccessor = variationContextAccessor; } + /// public IPublishedSnapshotAccessor PublishedSnapshotAccessor { get; } + + /// + /// Gets the + /// public IVariationContextAccessor VariationContextAccessor { get; } // note: NOT setting _publishedSnapshotAccessor.PublishedSnapshot here because it is the // responsibility of the caller to manage what the 'current' facade is + + /// public abstract IPublishedSnapshot CreatePublishedSnapshot(string previewToken); protected IPublishedSnapshot CurrentPublishedSnapshot => PublishedSnapshotAccessor.PublishedSnapshot; + /// public abstract bool EnsureEnvironment(out IEnumerable errors); + /// public abstract string EnterPreview(IUser user, int contentId); + + /// public abstract void RefreshPreview(string previewToken, int contentId); + + /// public abstract void ExitPreview(string previewToken); + + /// public abstract void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged); + + /// public abstract void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged); + + /// public abstract void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads); + + /// public abstract void Notify(DataTypeCacheRefresher.JsonPayload[] payloads); + + /// public abstract void Notify(DomainCacheRefresher.JsonPayload[] payloads); - public virtual void Rebuild() + // TODO: Why is this virtual? + + /// + public virtual void Rebuild( + int groupSize = 5000, + IReadOnlyCollection contentTypeIds = null, + IReadOnlyCollection mediaTypeIds = null, + IReadOnlyCollection memberTypeIds = null) { } + /// public virtual void Dispose() { } + /// public abstract string GetStatus(); + /// public virtual string StatusUrl => "views/dashboard/settings/publishedsnapshotcache.html"; + /// public virtual void Collect() { } + + public abstract void LoadCachesOnStartup(); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs index a611186021..2aa450b7b9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs @@ -1,4 +1,4 @@ -using System.Data; +using System.Data; using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 2533eaea8e..7ce363e446 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -767,8 +767,21 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region UnitOfWork Events - // TODO: The reason these events are in the repository is for legacy, the events should exist at the service - // level now since we can fire these events within the transaction... so move the events to service level + /* + * TODO: The reason these events are in the repository is for legacy, the events should exist at the service + * level now since we can fire these events within the transaction... + * The reason these events 'need' to fire in the transaction is to ensure data consistency with Nucache (currently + * the only thing that uses them). For example, if the transaction succeeds and NuCache listened to ContentService.Saved + * and then NuCache failed at persisting data after the trans completed, then NuCache would be out of sync. This way + * the entire trans is rolled back if NuCache files. That said, I'm unsure this is really required because there + * are other systems that rely on the "ed" (i.e. Saved) events like Examine which would be inconsistent if it failed + * too. I'm just not sure this is totally necessary especially. + * So these events can be moved to the service level. However, see the notes below, it seems the only event we + * really need is the ScopedEntityRefresh. The only tricky part with moving that to the service level is that the + * handlers of that event will need to deal with the data a little differently because it seems that the + * "Published" flag on the content item matters and this event is raised before that flag is switched. Weird. + * We have the ability with IContent to see if something "WasPublished", etc.. so i think we could still use that. + */ public class ScopedEntityEventArgs : EventArgs { @@ -784,6 +797,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public class ScopedVersionEventArgs : EventArgs { + /// + /// Initializes a new instance of the class. + /// public ScopedVersionEventArgs(IScope scope, int entityId, int versionId) { Scope = scope; @@ -791,13 +807,43 @@ namespace Umbraco.Core.Persistence.Repositories.Implement VersionId = versionId; } + /// + /// Gets the current + /// public IScope Scope { get; } + + /// + /// Gets the entity id + /// public int EntityId { get; } + + /// + /// Gets the version id + /// public int VersionId { get; } } + /// + /// Occurs when an is created or updated from within the (transaction) + /// public static event TypedEventHandler ScopedEntityRefresh; + + /// + /// Occurs when an is being deleted from within the (transaction) + /// + /// + /// TODO: This doesn't seem to be necessary at all, the service "Deleting" events for this would work just fine + /// since they are raised before the item is actually deleted just like this event. + /// public static event TypedEventHandler ScopeEntityRemove; + + /// + /// Occurs when a version for an is being deleted from within the (transaction) + /// + /// + /// TODO: This doesn't seem to be necessary at all, the service "DeletingVersions" events for this would work just fine + /// since they are raised before the item is actually deleted just like this event. + /// public static event TypedEventHandler ScopeVersionRemove; // used by tests to clear events @@ -808,20 +854,23 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ScopeVersionRemove = null; } + /// + /// Raises the event + /// protected void OnUowRefreshedEntity(ScopedEntityEventArgs args) - { - ScopedEntityRefresh.RaiseEvent(args, This); - } + => ScopedEntityRefresh.RaiseEvent(args, This); + /// + /// Raises the event + /// protected void OnUowRemovingEntity(ScopedEntityEventArgs args) - { - ScopeEntityRemove.RaiseEvent(args, This); - } + => ScopeEntityRemove.RaiseEvent(args, This); + /// + /// Raises the event + /// protected void OnUowRemovingVersion(ScopedVersionEventArgs args) - { - ScopeVersionRemove.RaiseEvent(args, This); - } + => ScopeVersionRemove.RaiseEvent(args, This); #endregion diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs index 61ced57149..41f6a065d4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs @@ -1,15 +1,16 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NPoco; +using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Scoping; -using static Umbraco.Core.Persistence.SqlExtensionsStatics; using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; +using static Umbraco.Core.Persistence.SqlExtensionsStatics; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -20,21 +21,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// Limited to objects that have a corresponding node (in umbracoNode table). /// Returns objects, i.e. lightweight representation of entities. /// - internal class EntityRepository : IEntityRepository + internal class EntityRepository : RepositoryBase, IEntityRepository { - private readonly IScopeAccessor _scopeAccessor; - - public EntityRepository(IScopeAccessor scopeAccessor) + public EntityRepository(IScopeAccessor scopeAccessor, AppCaches appCaches) + : base(scopeAccessor, appCaches) { - _scopeAccessor = scopeAccessor; } - protected IUmbracoDatabase Database => _scopeAccessor.AmbientScope.Database; - protected Sql Sql() => _scopeAccessor.AmbientScope.SqlContext.Sql(); - protected ISqlSyntaxProvider SqlSyntax => _scopeAccessor.AmbientScope.SqlContext.SqlSyntax; - #region Repository - + public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering) { @@ -49,17 +44,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var isMedia = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Media); var isMember = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Member); - var sql = GetBaseWhere(isContent, isMedia, isMember, false, s => + Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, s => { sqlCustomization?.Invoke(s); if (filter != null) { - foreach (var filterClause in filter.GetWhereClauses()) + foreach (Tuple filterClause in filter.GetWhereClauses()) + { s.Where(filterClause.Item1, filterClause.Item2); + } } - - }, objectTypes); ordering = ordering ?? Ordering.ByDefault(); @@ -75,7 +70,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // TODO: we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently - //no matter what we always must have node id ordered at the end + // no matter what we always must have node id ordered at the end sql = ordering.Direction == Direction.Ascending ? sql.OrderBy("NodeId") : sql.OrderByDescending("NodeId"); // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names @@ -102,7 +97,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private IEntitySlim GetEntity(Sql sql, bool isContent, bool isMedia, bool isMember) { - //isContent is going to return a 1:M result now with the variants so we need to do different things + // isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) { var cdtos = Database.Fetch(sql); @@ -164,7 +159,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia, bool isMember) { - //isContent is going to return a 1:M result now with the variants so we need to do different things + // isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) { var cdtos = Database.Fetch(sql); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs similarity index 63% rename from src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs rename to src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs index a9e8f4bb16..d7f6e63c55 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; +using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Querying; @@ -9,53 +10,38 @@ using Umbraco.Core.Scoping; namespace Umbraco.Core.Persistence.Repositories.Implement { + /// - /// Provides a base class to all repositories. + /// Provides a base class to all based repositories. /// - /// The type of the entity managed by this repository. /// The type of the entity's unique identifier. - public abstract class RepositoryBase : IReadWriteQueryRepository + /// The type of the entity managed by this repository. + public abstract class EntityRepositoryBase : RepositoryBase, IReadWriteQueryRepository where TEntity : class, IEntity { private IRepositoryCachePolicy _cachePolicy; - - protected RepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger> logger) - { - ScopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - AppCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches)); - } - - protected ILogger> Logger { get; } - - protected AppCaches AppCaches { get; } - - protected IAppPolicyCache GlobalIsolatedCache => AppCaches.IsolatedCaches.GetOrCreate(); - - protected IScopeAccessor ScopeAccessor { get; } - - protected IScope AmbientScope - { - get - { - var scope = ScopeAccessor.AmbientScope; - if (scope == null) - throw new InvalidOperationException("Cannot run a repository without an ambient scope."); - return scope; - } - } - - #region Static Queries - private IQuery _hasIdQuery; + private static RepositoryCachePolicyOptions s_defaultOptions; - #endregion - - protected virtual TId GetEntityId(TEntity entity) + /// + /// Initializes a new instance of the class. + /// + protected EntityRepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger> logger) + : base(scopeAccessor, appCaches) { - return (TId) (object) entity.Id; + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); } + /// + /// Gets the logger + /// + protected ILogger> Logger { get; } + + /// + /// Gets the isolated cache for the + /// + protected IAppPolicyCache GlobalIsolatedCache => AppCaches.IsolatedCaches.GetOrCreate(); + /// /// Gets the isolated cache. /// @@ -78,30 +64,34 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } } - // ReSharper disable once StaticMemberInGenericType - private static RepositoryCachePolicyOptions _defaultOptions; - // ReSharper disable once InconsistentNaming - protected virtual RepositoryCachePolicyOptions DefaultOptions - { - get - { - return _defaultOptions ?? (_defaultOptions + /// + /// Gets the default + /// + protected virtual RepositoryCachePolicyOptions DefaultOptions => s_defaultOptions ?? (s_defaultOptions = new RepositoryCachePolicyOptions(() => { // get count of all entities of current type (TEntity) to ensure cached result is correct // create query once if it is needed (no need for locking here) - query is static! - var query = _hasIdQuery ?? (_hasIdQuery = AmbientScope.SqlContext.Query().Where(x => x.Id != 0)); + IQuery query = _hasIdQuery ?? (_hasIdQuery = AmbientScope.SqlContext.Query().Where(x => x.Id != 0)); return PerformCount(query); })); - } - } + /// + /// Gets the node object type for the repository's entity + /// + protected abstract Guid NodeObjectTypeId { get; } + + /// + /// Gets the repository cache policy + /// protected IRepositoryCachePolicy CachePolicy { get { if (AppCaches == AppCaches.NoCache) + { return NoCacheRepositoryCachePolicy.Instance; + } // create the cache policy using IsolatedCache which is either global // or scoped depending on the repository cache mode for the current scope @@ -122,66 +112,101 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } } + /// + /// Get the entity id for the + /// + protected virtual TId GetEntityId(TEntity entity) + => (TId)(object)entity.Id; + + /// + /// Create the repository cache policy + /// protected virtual IRepositoryCachePolicy CreateCachePolicy() - { - return new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); - } + => new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); /// /// Adds or Updates an entity of type TEntity /// /// This method is backed by an cache - /// public virtual void Save(TEntity entity) { if (entity.HasIdentity == false) + { CachePolicy.Create(entity, PersistNewItem); + } else + { CachePolicy.Update(entity, PersistUpdatedItem); + } } /// /// Deletes the passed in entity /// - /// public virtual void Delete(TEntity entity) - { - CachePolicy.Delete(entity, PersistDeletedItem); - } + => CachePolicy.Delete(entity, PersistDeletedItem); protected abstract TEntity PerformGet(TId id); + protected abstract IEnumerable PerformGetAll(params TId[] ids); + protected abstract IEnumerable PerformGetByQuery(IQuery query); - protected abstract bool PerformExists(TId id); - protected abstract int PerformCount(IQuery query); protected abstract void PersistNewItem(TEntity item); - protected abstract void PersistUpdatedItem(TEntity item); - protected abstract void PersistDeletedItem(TEntity item); + protected abstract void PersistUpdatedItem(TEntity item); + + protected abstract Sql GetBaseQuery(bool isCount); // TODO: obsolete, use QueryType instead everywhere + + protected abstract string GetBaseWhereClause(); + + protected abstract IEnumerable GetDeleteClauses(); + + protected virtual bool PerformExists(TId id) + { + var sql = GetBaseQuery(true); + sql.Where(GetBaseWhereClause(), new { id = id }); + var count = Database.ExecuteScalar(sql); + return count == 1; + } + + protected virtual int PerformCount(IQuery query) + { + var sqlClause = GetBaseQuery(true); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate(); + + return Database.ExecuteScalar(sql); + } + + protected virtual void PersistDeletedItem(TEntity entity) + { + var deletes = GetDeleteClauses(); + foreach (var delete in deletes) + { + Database.Execute(delete, new { id = GetEntityId(entity) }); + } + + entity.DeleteDate = DateTime.Now; + } /// /// Gets an entity by the passed in Id utilizing the repository's cache policy /// - /// - /// public TEntity Get(TId id) - { - return CachePolicy.Get(id, PerformGet, PerformGetAll); - } + => CachePolicy.Get(id, PerformGet, PerformGetAll); /// /// Gets all entities of type TEntity or a list according to the passed in Ids /// - /// - /// public IEnumerable GetMany(params TId[] ids) { - //ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries + // ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries ids = ids.Distinct() - //don't query by anything that is a default of T (like a zero) + + // don't query by anything that is a default of T (like a zero) // TODO: I think we should enabled this in case accidental calls are made to get all with invalid ids - //.Where(x => Equals(x, default(TId)) == false) + // .Where(x => Equals(x, default(TId)) == false) .ToArray(); // can't query more than 2000 ids at a time... but if someone is really querying 2000+ entities, @@ -197,39 +222,27 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { entities.AddRange(CachePolicy.GetAll(groupOfIds.ToArray(), PerformGetAll)); } + return entities; } /// /// Gets a list of entities by the passed in query /// - /// - /// public IEnumerable Get(IQuery query) - { - return PerformGetByQuery(query) - //ensure we don't include any null refs in the returned collection! - .WhereNotNull(); - } + => PerformGetByQuery(query) + .WhereNotNull(); // ensure we don't include any null refs in the returned collection! /// /// Returns a boolean indicating whether an entity with the passed Id exists /// - /// - /// public bool Exists(TId id) - { - return CachePolicy.Exists(id, PerformExists, PerformGetAll); - } + => CachePolicy.Exists(id, PerformExists, PerformGetAll); /// /// Returns an integer with the count of entities found with the passed in query /// - /// - /// public int Count(IQuery query) - { - return PerformCount(query); - } + => PerformCount(query); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NPocoRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NPocoRepositoryBase.cs index 392e7bdf1f..ff820da577 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NPocoRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NPocoRepositoryBase.cs @@ -1,7 +1,7 @@ -using System; +using System; using System.Collections.Generic; -using NPoco; using Microsoft.Extensions.Logging; +using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Querying; @@ -13,9 +13,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// /// Represent an abstract Repository for NPoco based repositories /// - /// - /// - public abstract class NPocoRepositoryBase : RepositoryBase + public abstract class NPocoRepositoryBase : EntityRepositoryBase where TEntity : class, IEntity { /// @@ -24,58 +22,5 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected NPocoRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger> logger) : base(scopeAccessor, cache, logger) { } - - /// - /// Gets the repository's database. - /// - protected IUmbracoDatabase Database => AmbientScope.Database; - - /// - /// Gets the Sql context. - /// - protected ISqlContext SqlContext=> AmbientScope.SqlContext; - - protected Sql Sql() => SqlContext.Sql(); - protected Sql Sql(string sql, params object[] args) => SqlContext.Sql(sql, args); - protected ISqlSyntaxProvider SqlSyntax => SqlContext.SqlSyntax; - protected IQuery Query() => SqlContext.Query(); - - #region Abstract Methods - - protected abstract Sql GetBaseQuery(bool isCount); // TODO: obsolete, use QueryType instead everywhere - protected abstract string GetBaseWhereClause(); - protected abstract IEnumerable GetDeleteClauses(); - protected abstract Guid NodeObjectTypeId { get; } - protected abstract override void PersistNewItem(TEntity entity); - protected abstract override void PersistUpdatedItem(TEntity entity); - - #endregion - - protected override bool PerformExists(TId id) - { - var sql = GetBaseQuery(true); - sql.Where(GetBaseWhereClause(), new { id = id}); - var count = Database.ExecuteScalar(sql); - return count == 1; - } - - protected override int PerformCount(IQuery query) - { - var sqlClause = GetBaseQuery(true); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - return Database.ExecuteScalar(sql); - } - - protected override void PersistDeletedItem(TEntity entity) - { - var deletes = GetDeleteClauses(); - foreach (var delete in deletes) - { - Database.Execute(delete, new { id = GetEntityId(entity) }); - } - entity.DeleteDate = DateTime.Now; - } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs new file mode 100644 index 0000000000..8b9d8fe77c --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs @@ -0,0 +1,81 @@ +using System; +using NPoco; +using Umbraco.Core.Cache; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Persistence.Repositories.Implement +{ + /// + /// Base repository class for all instances + /// + public abstract class RepositoryBase : IRepository + { + /// + /// Initializes a new instance of the class. + /// + protected RepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches) + { + ScopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); + AppCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches)); + } + + /// + /// Gets the + /// + protected AppCaches AppCaches { get; } + + /// + /// Gets the + /// + protected IScopeAccessor ScopeAccessor { get; } + + /// + /// Gets the AmbientScope + /// + protected IScope AmbientScope + { + get + { + IScope scope = ScopeAccessor.AmbientScope; + if (scope == null) + { + throw new InvalidOperationException("Cannot run a repository without an ambient scope."); + } + + return scope; + } + } + + /// + /// Gets the repository's database. + /// + protected IUmbracoDatabase Database => AmbientScope.Database; + + /// + /// Gets the Sql context. + /// + protected ISqlContext SqlContext => AmbientScope.SqlContext; + + /// + /// Gets the + /// + protected ISqlSyntaxProvider SqlSyntax => SqlContext.SqlSyntax; + + /// + /// Creates an expression + /// + protected Sql Sql() => SqlContext.Sql(); + + /// + /// Creates a expression + /// + protected Sql Sql(string sql, params object[] args) => SqlContext.Sql(sql, args); + + /// + /// Creates a new query expression + /// + protected IQuery Query() => SqlContext.Query(); + } +} diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs index 3241fa9d0e..1bdd00f576 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Scoping; @@ -16,10 +16,23 @@ namespace Umbraco.Core.Services.Implement protected abstract TService This { get; } - // that one must be dispatched + /// + /// Raised when a is changed + /// + /// + /// This event is dispatched after the trans is completed. Used by event refreshers. + /// public static event TypedEventHandler.EventArgs> Changed; - // that one is always immediate (transactional) + /// + /// Occurs when an is created or updated from within the (transaction) + /// + /// + /// The purpose of this event being raised within the transaction is so that listeners can perform database + /// operations from within the same transaction and guarantee data consistency so that if anything goes wrong + /// the entire transaction can be rolled back. This is used by Nucache. + /// TODO: See remarks in ContentRepositoryBase about these types of events. Not sure we need/want them. + /// public static event TypedEventHandler.EventArgs> ScopedRefreshedEntity; // used by tests to clear events @@ -45,9 +58,11 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(Changed, This, args, nameof(Changed)); } + /// + /// Raises the event during the (transaction) + /// protected void OnUowRefreshedEntity(ContentTypeChange.EventArgs args) { - // that one is always immediate (not dispatched, transactional) ScopedRefreshedEntity.RaiseEvent(args, This); } diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs index 81869a9261..344e1b025a 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Microsoft.Extensions.DependencyInjection; using Umbraco.Core.Configuration; using Umbraco.Core; @@ -11,6 +11,7 @@ using Umbraco.Core.DependencyInjection; namespace Umbraco.ModelsBuilder.Embedded.Compose { + // TODO: We'll need to change this stuff to IUmbracoBuilder ext and control the order of things there [ComposeBefore(typeof(IPublishedCacheComposer))] public sealed class ModelsBuilderComposer : ICoreComposer { diff --git a/src/Umbraco.PublishedCache.NuCache/ContentCache.cs b/src/Umbraco.PublishedCache.NuCache/ContentCache.cs index 7cdc7e72e1..f1c4f1d85b 100644 --- a/src/Umbraco.PublishedCache.NuCache/ContentCache.cs +++ b/src/Umbraco.PublishedCache.NuCache/ContentCache.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentNestedData.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentNestedData.cs index ec5424ad9a..98d423680b 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentNestedData.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentNestedData.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using System.Collections.Generic; using Umbraco.Core.Serialization; @@ -9,7 +9,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// internal class ContentNestedData { - //dont serialize empty properties + // dont serialize empty properties [JsonProperty("pd")] [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] public Dictionary PropertyData { get; set; } @@ -21,7 +21,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource [JsonProperty("us")] public string UrlSegment { get; set; } - //Legacy properties used to deserialize existing nucache db entries + // Legacy properties used to deserialize existing nucache db entries [JsonProperty("properties")] [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] private Dictionary LegacyPropertyData { set { PropertyData = value; } } diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/DatabaseDataSource.cs deleted file mode 100644 index bdcd8fe3e3..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/DatabaseDataSource.cs +++ /dev/null @@ -1,325 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using NPoco; -using Umbraco.Core; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Dtos; -using Umbraco.Core.Scoping; -using Umbraco.Core.Serialization; -using static Umbraco.Core.Persistence.SqlExtensionsStatics; - -namespace Umbraco.Web.PublishedCache.NuCache.DataSource -{ - // TODO: use SqlTemplate for these queries else it's going to be horribly slow! - - // provides efficient database access for NuCache - internal class DatabaseDataSource : IDataSource - { - private const int PageSize = 500; - - private readonly ILogger _logger; - - public DatabaseDataSource(ILogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - // we want arrays, we want them all loaded, not an enumerable - - private Sql ContentSourcesSelect(IScope scope, Func, Sql> joins = null) - { - var sql = scope.SqlContext.Sql() - - .Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), - x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), - x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) - .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) - .AndSelect(x => Alias(x.Published, "Published"), x => Alias(x.Edited, "Edited")) - - .AndSelect(x => Alias(x.Id, "VersionId"), x => Alias(x.Text, "EditName"), x => Alias(x.VersionDate, "EditVersionDate"), x => Alias(x.UserId, "EditWriterId")) - .AndSelect(x => Alias(x.TemplateId, "EditTemplateId")) - - .AndSelect("pcver", x => Alias(x.Id, "PublishedVersionId"), x => Alias(x.Text, "PubName"), x => Alias(x.VersionDate, "PubVersionDate"), x => Alias(x.UserId, "PubWriterId")) - .AndSelect("pdver", x => Alias(x.TemplateId, "PubTemplateId")) - - .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) - .AndSelect("nuPub", x => Alias(x.Data, "PubData")) - - .From(); - - if (joins != null) - sql = joins(sql); - - sql = sql - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - - .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) - .InnerJoin().On((left, right) => left.Id == right.Id) - - .LeftJoin(j => - j.InnerJoin("pdver").On((left, right) => left.Id == right.Id && right.Published, "pcver", "pdver"), "pcver") - .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcver") - - .LeftJoin("nuEdit").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuEdit") - .LeftJoin("nuPub").On((left, right) => left.NodeId == right.NodeId && right.Published, aliasRight: "nuPub"); - - return sql; - } - - public ContentNodeKit GetContentSource(IScope scope, int id) - { - var sql = ContentSourcesSelect(scope) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && x.NodeId == id && !x.Trashed) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - - var dto = scope.Database.Fetch(sql).FirstOrDefault(); - return dto == null ? new ContentNodeKit() : CreateContentNodeKit(dto); - } - - public IEnumerable GetAllContentSources(IScope scope) - { - var sql = ContentSourcesSelect(scope) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - - // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. - // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - - foreach (var row in scope.Database.QueryPaged(PageSize, sql)) - yield return CreateContentNodeKit(row); - } - - public IEnumerable GetBranchContentSources(IScope scope, int id) - { - var syntax = scope.SqlContext.SqlSyntax; - var sql = ContentSourcesSelect(scope, - s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) - .Where(x => x.NodeId == id, "x") - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - - // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. - // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - - foreach (var row in scope.Database.QueryPaged(PageSize, sql)) - yield return CreateContentNodeKit(row); - } - - public IEnumerable GetTypeContentSources(IScope scope, IEnumerable ids) - { - if (!ids.Any()) yield break; - - var sql = ContentSourcesSelect(scope) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) - .WhereIn(x => x.ContentTypeId, ids) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - - // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. - // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - - foreach (var row in scope.Database.QueryPaged(PageSize, sql)) - yield return CreateContentNodeKit(row); - } - - private Sql MediaSourcesSelect(IScope scope, Func, Sql> joins = null) - { - var sql = scope.SqlContext.Sql() - - .Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), - x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), - x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) - .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) - .AndSelect(x => Alias(x.Id, "VersionId"), x => Alias(x.Text, "EditName"), x => Alias(x.VersionDate, "EditVersionDate"), x => Alias(x.UserId, "EditWriterId")) - .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) - .From(); - - if (joins != null) - sql = joins(sql); - - sql = sql - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) - .LeftJoin("nuEdit").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuEdit"); - - return sql; - } - - public ContentNodeKit GetMediaSource(IScope scope, int id) - { - var sql = MediaSourcesSelect(scope) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && x.NodeId == id && !x.Trashed) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - - var dto = scope.Database.Fetch(sql).FirstOrDefault(); - return dto == null ? new ContentNodeKit() : CreateMediaNodeKit(dto); - } - - public IEnumerable GetAllMediaSources(IScope scope) - { - var sql = MediaSourcesSelect(scope) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - - // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. - // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - - foreach (var row in scope.Database.QueryPaged(PageSize, sql)) - yield return CreateMediaNodeKit(row); - } - - public IEnumerable GetBranchMediaSources(IScope scope, int id) - { - var syntax = scope.SqlContext.SqlSyntax; - var sql = MediaSourcesSelect(scope, - s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) - .Where(x => x.NodeId == id, "x") - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - - // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. - // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - - foreach (var row in scope.Database.QueryPaged(PageSize, sql)) - yield return CreateMediaNodeKit(row); - } - - public IEnumerable GetTypeMediaSources(IScope scope, IEnumerable ids) - { - if (!ids.Any()) yield break; - - var sql = MediaSourcesSelect(scope) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) - .WhereIn(x => x.ContentTypeId, ids) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - - // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. - // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - - foreach (var row in scope.Database.QueryPaged(PageSize, sql)) - yield return CreateMediaNodeKit(row); - } - - private ContentNodeKit CreateContentNodeKit(ContentSourceDto dto) - { - ContentData d = null; - ContentData p = null; - - if (dto.Edited) - { - if (dto.EditData == null) - { - if (Debugger.IsAttached) - throw new InvalidOperationException("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding."); - _logger.LogWarning("Missing cmsContentNu edited content for node {NodeId}, consider rebuilding.", dto.Id); - } - else - { - var nested = DeserializeNestedData(dto.EditData); - - d = new ContentData - { - Name = dto.EditName, - Published = false, - TemplateId = dto.EditTemplateId, - VersionId = dto.VersionId, - VersionDate = dto.EditVersionDate, - WriterId = dto.EditWriterId, - Properties = nested.PropertyData, - CultureInfos = nested.CultureData, - UrlSegment = nested.UrlSegment - }; - } - } - - if (dto.Published) - { - if (dto.PubData == null) - { - if (Debugger.IsAttached) - throw new InvalidOperationException("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding."); - _logger.LogWarning("Missing cmsContentNu published content for node {NodeId}, consider rebuilding.", dto.Id); - } - else - { - var nested = DeserializeNestedData(dto.PubData); - - p = new ContentData - { - Name = dto.PubName, - UrlSegment = nested.UrlSegment, - Published = true, - TemplateId = dto.PubTemplateId, - VersionId = dto.VersionId, - VersionDate = dto.PubVersionDate, - WriterId = dto.PubWriterId, - Properties = nested.PropertyData, - CultureInfos = nested.CultureData - }; - } - } - - var n = new ContentNode(dto.Id, dto.Uid, - dto.Level, dto.Path, dto.SortOrder, dto.ParentId, dto.CreateDate, dto.CreatorId); - - var s = new ContentNodeKit - { - Node = n, - ContentTypeId = dto.ContentTypeId, - DraftData = d, - PublishedData = p - }; - - return s; - } - - private static ContentNodeKit CreateMediaNodeKit(ContentSourceDto dto) - { - if (dto.EditData == null) - throw new InvalidOperationException("No data for media " + dto.Id); - - var nested = DeserializeNestedData(dto.EditData); - - var p = new ContentData - { - Name = dto.EditName, - Published = true, - TemplateId = -1, - VersionId = dto.VersionId, - VersionDate = dto.EditVersionDate, - WriterId = dto.CreatorId, // what-else? - Properties = nested.PropertyData, - CultureInfos = nested.CultureData - }; - - var n = new ContentNode(dto.Id, dto.Uid, - dto.Level, dto.Path, dto.SortOrder, dto.ParentId, dto.CreateDate, dto.CreatorId); - - var s = new ContentNodeKit - { - Node = n, - ContentTypeId = dto.ContentTypeId, - PublishedData = p - }; - - return s; - } - - private static ContentNestedData DeserializeNestedData(string data) - { - // by default JsonConvert will deserialize our numeric values as Int64 - // which is bad, because they were Int32 in the database - take care - - var settings = new JsonSerializerSettings - { - Converters = new List { new ForceInt32Converter() } - }; - - return JsonConvert.DeserializeObject(data, settings); - } - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/IDataSource.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/IDataSource.cs deleted file mode 100644 index ec3ab38e84..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/IDataSource.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core.Scoping; - -namespace Umbraco.Web.PublishedCache.NuCache.DataSource -{ - /// - /// Defines a data source for NuCache. - /// - internal interface IDataSource - { - //TODO: For these required sort orders, would sorting on Path 'just work'? - - ContentNodeKit GetContentSource(IScope scope, int id); - - /// - /// Returns all content ordered by level + sortOrder - /// - /// - /// - /// - /// MUST be ordered by level + parentId + sortOrder! - /// - IEnumerable GetAllContentSources(IScope scope); - - /// - /// Returns branch for content ordered by level + sortOrder - /// - /// - /// - /// - /// MUST be ordered by level + parentId + sortOrder! - /// - IEnumerable GetBranchContentSources(IScope scope, int id); - - /// - /// Returns content by Ids ordered by level + sortOrder - /// - /// - /// - /// - /// MUST be ordered by level + parentId + sortOrder! - /// - IEnumerable GetTypeContentSources(IScope scope, IEnumerable ids); - - ContentNodeKit GetMediaSource(IScope scope, int id); - - /// - /// Returns all media ordered by level + sortOrder - /// - /// - /// - /// - /// MUST be ordered by level + parentId + sortOrder! - /// - IEnumerable GetAllMediaSources(IScope scope); - - /// - /// Returns branch for media ordered by level + sortOrder - /// - /// - /// - /// - /// MUST be ordered by level + parentId + sortOrder! - /// - IEnumerable GetBranchMediaSources(IScope scope, int id); // must order by level, sortOrder - - /// - /// Returns media by Ids ordered by level + sortOrder - /// - /// - /// - /// - /// MUST be ordered by level + parentId + sortOrder! - /// - IEnumerable GetTypeMediaSources(IScope scope, IEnumerable ids); - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs index cf7ab95360..42e038c744 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using Newtonsoft.Json; @@ -29,7 +29,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public object Value { get; set; } - //Legacy properties used to deserialize existing nucache db entries + // Legacy properties used to deserialize existing nucache db entries [JsonProperty("culture")] private string LegacyCulture { diff --git a/src/Umbraco.PublishedCache.NuCache/NuCacheComponent.cs b/src/Umbraco.PublishedCache.NuCache/NuCacheComponent.cs deleted file mode 100644 index fba133a2aa..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/NuCacheComponent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Umbraco.Core.Composing; - -namespace Umbraco.Web.PublishedCache.NuCache -{ - public sealed class NuCacheComponent : IComponent - { - public NuCacheComponent(IPublishedSnapshotService service) - { - // nothing - this just ensures that the service is created at boot time - } - - public void Initialize() - { } - - public void Terminate() - { } - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/NuCacheComposer.cs b/src/Umbraco.PublishedCache.NuCache/NuCacheComposer.cs index 17e707effd..5e618d361c 100644 --- a/src/Umbraco.PublishedCache.NuCache/NuCacheComposer.cs +++ b/src/Umbraco.PublishedCache.NuCache/NuCacheComposer.cs @@ -1,23 +1,26 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Core; -using Umbraco.Core.DependencyInjection; using Umbraco.Core.Composing; +using Umbraco.Core.DependencyInjection; using Umbraco.Core.Models; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Infrastructure.PublishedCache; -using Umbraco.Web.PublishedCache.NuCache.DataSource; +using Umbraco.Infrastructure.PublishedCache.Persistence; namespace Umbraco.Web.PublishedCache.NuCache { - public class NuCacheComposer : ComponentComposer, IPublishedCacheComposer + // TODO: We'll need to change this stuff to IUmbracoBuilder ext and control the order of things there, + // see comment in ModelsBuilderComposer which requires this weird IPublishedCacheComposer + public class NuCacheComposer : IComposer, IPublishedCacheComposer { - public override void Compose(IUmbracoBuilder builder) + /// + public void Compose(IUmbracoBuilder builder) { - base.Compose(builder); - // register the NuCache database data source - builder.Services.AddTransient(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // register the NuCache published snapshot service // must register default options, required in the service ctor @@ -26,21 +29,22 @@ namespace Umbraco.Web.PublishedCache.NuCache // replace this service since we want to improve the content/media // mapping lookups if we are using nucache. + // TODO: Gotta wonder how much this does actually improve perf? It's a lot of weird code to make this happen so hope it's worth it builder.Services.AddUnique(factory => { var idkSvc = new IdKeyMap(factory.GetRequiredService()); - var publishedSnapshotService = factory.GetRequiredService() as PublishedSnapshotService; - if (publishedSnapshotService != null) + if (factory.GetRequiredService() is PublishedSnapshotService publishedSnapshotService) { idkSvc.SetMapper(UmbracoObjectTypes.Document, id => publishedSnapshotService.GetDocumentUid(id), uid => publishedSnapshotService.GetDocumentId(uid)); idkSvc.SetMapper(UmbracoObjectTypes.Media, id => publishedSnapshotService.GetMediaUid(id), uid => publishedSnapshotService.GetMediaId(uid)); } + return idkSvc; }); // add the NuCache health check (hidden from type finder) // TODO: no NuCache health check yet - //composition.HealthChecks().Add(); + // composition.HealthChecks().Add(); } } } diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentRepository.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentRepository.cs new file mode 100644 index 0000000000..7bce5e138c --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentRepository.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; +using Umbraco.Web.PublishedCache.NuCache; + +namespace Umbraco.Infrastructure.PublishedCache.Persistence +{ + public interface INuCacheContentRepository + { + void DeleteContentItem(IContentBase item); + IEnumerable GetAllContentSources(); + IEnumerable GetAllMediaSources(); + IEnumerable GetBranchContentSources(int id); + IEnumerable GetBranchMediaSources(int id); + ContentNodeKit GetContentSource(int id); + ContentNodeKit GetMediaSource(int id); + IEnumerable GetTypeContentSources(IEnumerable ids); + IEnumerable GetTypeMediaSources(IEnumerable ids); + + /// + /// Refreshes the nucache database row for the + /// + void RefreshContent(IContent content); + + /// + /// Refreshes the nucache database row for the (used for media/members) + /// + void RefreshEntity(IContentBase content); + + /// + /// Rebuilds the caches for content, media and/or members based on the content type ids specified + /// + /// The operation batch size to process the items + /// If not null will process content for the matching content types, if empty will process all content + /// If not null will process content for the matching media types, if empty will process all media + /// If not null will process content for the matching members types, if empty will process all members + void Rebuild( + int groupSize = 5000, + IReadOnlyCollection contentTypeIds = null, + IReadOnlyCollection mediaTypeIds = null, + IReadOnlyCollection memberTypeIds = null); + + bool VerifyContentDbCache(); + bool VerifyMediaDbCache(); + bool VerifyMemberDbCache(); + } +} diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentService.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentService.cs new file mode 100644 index 0000000000..0ac3939742 --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentService.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; +using Umbraco.Web.PublishedCache.NuCache; + +namespace Umbraco.Infrastructure.PublishedCache.Persistence +{ + /// + /// Defines a data source for NuCache. + /// + public interface INuCacheContentService + { + // TODO: For these required sort orders, would sorting on Path 'just work'? + ContentNodeKit GetContentSource(int id); + + /// + /// Returns all content ordered by level + sortOrder + /// + /// + /// MUST be ordered by level + parentId + sortOrder! + /// + IEnumerable GetAllContentSources(); + + /// + /// Returns branch for content ordered by level + sortOrder + /// + /// + /// MUST be ordered by level + parentId + sortOrder! + /// + IEnumerable GetBranchContentSources(int id); + + /// + /// Returns content by Ids ordered by level + sortOrder + /// + /// + /// MUST be ordered by level + parentId + sortOrder! + /// + IEnumerable GetTypeContentSources(IEnumerable ids); + + ContentNodeKit GetMediaSource(int id); + + /// + /// Returns all media ordered by level + sortOrder + /// + /// + /// MUST be ordered by level + parentId + sortOrder! + /// + IEnumerable GetAllMediaSources(); + + /// + /// Returns branch for media ordered by level + sortOrder + /// + /// + /// MUST be ordered by level + parentId + sortOrder! + /// + IEnumerable GetBranchMediaSources(int id); // must order by level, sortOrder + + /// + /// Returns media by Ids ordered by level + sortOrder + /// + /// + /// MUST be ordered by level + parentId + sortOrder! + /// + IEnumerable GetTypeMediaSources(IEnumerable ids); + + void DeleteContentItem(IContentBase item); + + /// + /// Refreshes the nucache database row for the + /// + void RefreshContent(IContent content); + + /// + /// Refreshes the nucache database row for the (used for media/members) + /// + void RefreshEntity(IContentBase content); + + /// + /// Rebuilds the caches for content, media and/or members based on the content type ids specified + /// + /// The operation batch size to process the items + /// If not null will process content for the matching content types, if empty will process all content + /// If not null will process content for the matching media types, if empty will process all media + /// If not null will process content for the matching members types, if empty will process all members + void Rebuild( + int groupSize = 5000, + IReadOnlyCollection contentTypeIds = null, + IReadOnlyCollection mediaTypeIds = null, + IReadOnlyCollection memberTypeIds = null); + + bool VerifyContentDbCache(); + + bool VerifyMediaDbCache(); + + bool VerifyMemberDbCache(); + } +} diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs new file mode 100644 index 0000000000..60370e9be8 --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs @@ -0,0 +1,735 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using NPoco; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.Scoping; +using Umbraco.Core.Serialization; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.Web.PublishedCache.NuCache; +using Umbraco.Web.PublishedCache.NuCache.DataSource; +using static Umbraco.Core.Persistence.SqlExtensionsStatics; + +namespace Umbraco.Infrastructure.PublishedCache.Persistence +{ + public class NuCacheContentRepository : RepositoryBase, INuCacheContentRepository + { + private const int PageSize = 500; + private readonly ILogger _logger; + private readonly IMemberRepository _memberRepository; + private readonly IDocumentRepository _documentRepository; + private readonly IMediaRepository _mediaRepository; + private readonly IShortStringHelper _shortStringHelper; + private readonly UrlSegmentProviderCollection _urlSegmentProviders; + + /// + /// Initializes a new instance of the class. + /// + public NuCacheContentRepository( + IScopeAccessor scopeAccessor, + AppCaches appCaches, + ILogger logger, + IMemberRepository memberRepository, + IDocumentRepository documentRepository, + IMediaRepository mediaRepository, + IShortStringHelper shortStringHelper, + UrlSegmentProviderCollection urlSegmentProviders) + : base(scopeAccessor, appCaches) + { + _logger = logger; + _memberRepository = memberRepository; + _documentRepository = documentRepository; + _mediaRepository = mediaRepository; + _shortStringHelper = shortStringHelper; + _urlSegmentProviders = urlSegmentProviders; + } + + public void DeleteContentItem(IContentBase item) + => Database.Execute("DELETE FROM cmsContentNu WHERE nodeId=@id", new { id = item.Id }); + + public void RefreshContent(IContent content) + { + // always refresh the edited data + OnRepositoryRefreshed(content, false); + + if (content.PublishedState == PublishedState.Unpublishing) + { + // if unpublishing, remove published data from table + Database.Execute("DELETE FROM cmsContentNu WHERE nodeId=@id AND published=1", new { id = content.Id }); + } + else if (content.PublishedState == PublishedState.Publishing) + { + // if publishing, refresh the published data + OnRepositoryRefreshed(content, true); + } + } + + public void RefreshEntity(IContentBase content) + => OnRepositoryRefreshed(content, false); + + private void OnRepositoryRefreshed(IContentBase content, bool published) + { + // use a custom SQL to update row version on each update + // db.InsertOrUpdate(dto); + ContentNuDto dto = GetDto(content, published); + + Database.InsertOrUpdate( + dto, + "SET data=@data, rv=rv+1 WHERE nodeId=@id AND published=@published", + new + { + data = dto.Data, + id = dto.NodeId, + published = dto.Published + }); + } + + public void Rebuild( + int groupSize = 5000, + IReadOnlyCollection contentTypeIds = null, + IReadOnlyCollection mediaTypeIds = null, + IReadOnlyCollection memberTypeIds = null) + { + if (contentTypeIds != null) + { + RebuildContentDbCache(groupSize, contentTypeIds); + } + + if (mediaTypeIds != null) + { + RebuildContentDbCache(groupSize, mediaTypeIds); + } + + if (memberTypeIds != null) + { + RebuildContentDbCache(groupSize, memberTypeIds); + } + } + + // assumes content tree lock + private void RebuildContentDbCache(int groupSize, IReadOnlyCollection contentTypeIds) + { + Guid contentObjectType = Constants.ObjectTypes.Document; + + // remove all - if anything fails the transaction will rollback + if (contentTypeIds == null || contentTypeIds.Count == 0) + { + // must support SQL-CE + Database.Execute( + @"DELETE FROM cmsContentNu +WHERE cmsContentNu.nodeId IN ( + SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType +)", + new { objType = contentObjectType }); + } + else + { + // assume number of ctypes won't blow IN(...) + // must support SQL-CE + Database.Execute( + $@"DELETE FROM cmsContentNu +WHERE cmsContentNu.nodeId IN ( + SELECT id FROM umbracoNode + JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id + WHERE umbracoNode.nodeObjectType=@objType + AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes) +)", + new { objType = contentObjectType, ctypes = contentTypeIds }); + } + + // insert back - if anything fails the transaction will rollback + IQuery query = SqlContext.Query(); + if (contentTypeIds != null && contentTypeIds.Count > 0) + { + query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...) + } + + long pageIndex = 0; + long processed = 0; + long total; + do + { + // the tree is locked, counting and comparing to total is safe + IEnumerable descendants = _documentRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); + var items = new List(); + var count = 0; + foreach (IContent c in descendants) + { + // always the edited version + items.Add(GetDto(c, false)); + + // and also the published version if it makes any sense + if (c.Published) + { + items.Add(GetDto(c, true)); + } + + count++; + } + + Database.BulkInsertRecords(items); + processed += count; + } while (processed < total); + } + + // assumes media tree lock + private void RebuildMediaDbCache(int groupSize, IReadOnlyCollection contentTypeIds) + { + var mediaObjectType = Constants.ObjectTypes.Media; + + // remove all - if anything fails the transaction will rollback + if (contentTypeIds == null || contentTypeIds.Count == 0) + { + // must support SQL-CE + Database.Execute( + @"DELETE FROM cmsContentNu +WHERE cmsContentNu.nodeId IN ( + SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType +)", + new { objType = mediaObjectType }); + } + else + { + // assume number of ctypes won't blow IN(...) + // must support SQL-CE + Database.Execute( + $@"DELETE FROM cmsContentNu +WHERE cmsContentNu.nodeId IN ( + SELECT id FROM umbracoNode + JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id + WHERE umbracoNode.nodeObjectType=@objType + AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes) +)", + new { objType = mediaObjectType, ctypes = contentTypeIds }); + } + + // insert back - if anything fails the transaction will rollback + var query = SqlContext.Query(); + if (contentTypeIds != null && contentTypeIds.Count > 0) + { + query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...) + } + + long pageIndex = 0; + long processed = 0; + long total; + do + { + // the tree is locked, counting and comparing to total is safe + var descendants = _mediaRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); + var items = descendants.Select(m => GetDto(m, false)).ToList(); + Database.BulkInsertRecords(items); + processed += items.Count; + } while (processed < total); + } + + // assumes member tree lock + private void RebuildMemberDbCache(int groupSize, IReadOnlyCollection contentTypeIds) + { + Guid memberObjectType = Constants.ObjectTypes.Member; + + // remove all - if anything fails the transaction will rollback + if (contentTypeIds == null || contentTypeIds.Count == 0) + { + // must support SQL-CE + Database.Execute( + @"DELETE FROM cmsContentNu +WHERE cmsContentNu.nodeId IN ( + SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType +)", + new { objType = memberObjectType }); + } + else + { + // assume number of ctypes won't blow IN(...) + // must support SQL-CE + Database.Execute( + $@"DELETE FROM cmsContentNu +WHERE cmsContentNu.nodeId IN ( + SELECT id FROM umbracoNode + JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id + WHERE umbracoNode.nodeObjectType=@objType + AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes) +)", + new { objType = memberObjectType, ctypes = contentTypeIds }); + } + + // insert back - if anything fails the transaction will rollback + IQuery query = SqlContext.Query(); + if (contentTypeIds != null && contentTypeIds.Count > 0) + { + query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...) + } + + long pageIndex = 0; + long processed = 0; + long total; + do + { + IEnumerable descendants = _memberRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); + ContentNuDto[] items = descendants.Select(m => GetDto(m, false)).ToArray(); + Database.BulkInsertRecords(items); + processed += items.Length; + } while (processed < total); + } + + // assumes content tree lock + public bool VerifyContentDbCache() + { + // every document should have a corresponding row for edited properties + // and if published, may have a corresponding row for published properties + Guid contentObjectType = Constants.ObjectTypes.Document; + + var count = Database.ExecuteScalar( + $@"SELECT COUNT(*) +FROM umbracoNode +JOIN {Constants.DatabaseSchema.Tables.Document} ON umbracoNode.id={Constants.DatabaseSchema.Tables.Document}.nodeId +LEFT JOIN cmsContentNu nuEdited ON (umbracoNode.id=nuEdited.nodeId AND nuEdited.published=0) +LEFT JOIN cmsContentNu nuPublished ON (umbracoNode.id=nuPublished.nodeId AND nuPublished.published=1) +WHERE umbracoNode.nodeObjectType=@objType +AND nuEdited.nodeId IS NULL OR ({Constants.DatabaseSchema.Tables.Document}.published=1 AND nuPublished.nodeId IS NULL);", + new { objType = contentObjectType }); + + return count == 0; + } + + // assumes media tree lock + public bool VerifyMediaDbCache() + { + // every media item should have a corresponding row for edited properties + Guid mediaObjectType = Constants.ObjectTypes.Media; + + var count = Database.ExecuteScalar( + @"SELECT COUNT(*) +FROM umbracoNode +LEFT JOIN cmsContentNu ON (umbracoNode.id=cmsContentNu.nodeId AND cmsContentNu.published=0) +WHERE umbracoNode.nodeObjectType=@objType +AND cmsContentNu.nodeId IS NULL +", new { objType = mediaObjectType }); + + return count == 0; + } + + // assumes member tree lock + public bool VerifyMemberDbCache() + { + // every member item should have a corresponding row for edited properties + var memberObjectType = Constants.ObjectTypes.Member; + + var count = Database.ExecuteScalar( + @"SELECT COUNT(*) +FROM umbracoNode +LEFT JOIN cmsContentNu ON (umbracoNode.id=cmsContentNu.nodeId AND cmsContentNu.published=0) +WHERE umbracoNode.nodeObjectType=@objType +AND cmsContentNu.nodeId IS NULL +", new { objType = memberObjectType }); + + return count == 0; + } + + private ContentNuDto GetDto(IContentBase content, bool published) + { + // should inject these in ctor + // BUT for the time being we decide not to support ConvertDbToXml/String + // var propertyEditorResolver = PropertyEditorResolver.Current; + // var dataTypeService = ApplicationContext.Current.Services.DataTypeService; + var propertyData = new Dictionary(); + foreach (IProperty prop in content.Properties) + { + var pdatas = new List(); + foreach (IPropertyValue pvalue in prop.Values) + { + // sanitize - properties should be ok but ... never knows + if (!prop.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) + { + continue; + } + + // note: at service level, invariant is 'null', but here invariant becomes 'string.Empty' + var value = published ? pvalue.PublishedValue : pvalue.EditedValue; + if (value != null) + { + pdatas.Add(new PropertyData { Culture = pvalue.Culture ?? string.Empty, Segment = pvalue.Segment ?? string.Empty, Value = value }); + } + } + + propertyData[prop.Alias] = pdatas.ToArray(); + } + + var cultureData = new Dictionary(); + + // sanitize - names should be ok but ... never knows + if (content.ContentType.VariesByCulture()) + { + ContentCultureInfosCollection infos = content is IContent document + ? published + ? document.PublishCultureInfos + : document.CultureInfos + : content.CultureInfos; + + // ReSharper disable once UseDeconstruction + foreach (ContentCultureInfos cultureInfo in infos) + { + var cultureIsDraft = !published && content is IContent d && d.IsCultureEdited(cultureInfo.Culture); + cultureData[cultureInfo.Culture] = new CultureVariation + { + Name = cultureInfo.Name, + UrlSegment = content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders, cultureInfo.Culture), + Date = content.GetUpdateDate(cultureInfo.Culture) ?? DateTime.MinValue, + IsDraft = cultureIsDraft + }; + } + } + + // the dictionary that will be serialized + var nestedData = new ContentNestedData + { + PropertyData = propertyData, + CultureData = cultureData, + UrlSegment = content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders) + }; + + var dto = new ContentNuDto + { + NodeId = content.Id, + Published = published, + + // note that numeric values (which are Int32) are serialized without their + // type (eg "value":1234) and JsonConvert by default deserializes them as Int64 + Data = JsonConvert.SerializeObject(nestedData) + }; + + return dto; + } + + // we want arrays, we want them all loaded, not an enumerable + private Sql ContentSourcesSelect(Func, Sql> joins = null) + { + var sql = Sql() + + .Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), + x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), + x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) + .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) + .AndSelect(x => Alias(x.Published, "Published"), x => Alias(x.Edited, "Edited")) + + .AndSelect(x => Alias(x.Id, "VersionId"), x => Alias(x.Text, "EditName"), x => Alias(x.VersionDate, "EditVersionDate"), x => Alias(x.UserId, "EditWriterId")) + .AndSelect(x => Alias(x.TemplateId, "EditTemplateId")) + + .AndSelect("pcver", x => Alias(x.Id, "PublishedVersionId"), x => Alias(x.Text, "PubName"), x => Alias(x.VersionDate, "PubVersionDate"), x => Alias(x.UserId, "PubWriterId")) + .AndSelect("pdver", x => Alias(x.TemplateId, "PubTemplateId")) + + .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) + .AndSelect("nuPub", x => Alias(x.Data, "PubData")) + + .From(); + + if (joins != null) + { + sql = joins(sql); + } + + sql = sql + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .InnerJoin().On((left, right) => left.Id == right.Id) + + .LeftJoin(j => + j.InnerJoin("pdver").On((left, right) => left.Id == right.Id && right.Published, "pcver", "pdver"), "pcver") + .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcver") + + .LeftJoin("nuEdit").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuEdit") + .LeftJoin("nuPub").On((left, right) => left.NodeId == right.NodeId && right.Published, aliasRight: "nuPub"); + + return sql; + } + + public ContentNodeKit GetContentSource(int id) + { + var sql = ContentSourcesSelect() + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && x.NodeId == id && !x.Trashed) + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + + var dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? new ContentNodeKit() : CreateContentNodeKit(dto); + } + + public IEnumerable GetAllContentSources() + { + var sql = ContentSourcesSelect() + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in Database.QueryPaged(PageSize, sql)) + { + yield return CreateContentNodeKit(row); + } + } + + public IEnumerable GetBranchContentSources(int id) + { + var syntax = SqlSyntax; + var sql = ContentSourcesSelect( + s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) + .Where(x => x.NodeId == id, "x") + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in Database.QueryPaged(PageSize, sql)) + { + yield return CreateContentNodeKit(row); + } + } + + public IEnumerable GetTypeContentSources(IEnumerable ids) + { + if (!ids.Any()) + yield break; + + var sql = ContentSourcesSelect() + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) + .WhereIn(x => x.ContentTypeId, ids) + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in Database.QueryPaged(PageSize, sql)) + { + yield return CreateContentNodeKit(row); + } + } + + private Sql MediaSourcesSelect(Func, Sql> joins = null) + { + var sql = Sql() + + .Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), + x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), + x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) + .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) + .AndSelect(x => Alias(x.Id, "VersionId"), x => Alias(x.Text, "EditName"), x => Alias(x.VersionDate, "EditVersionDate"), x => Alias(x.UserId, "EditWriterId")) + .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) + .From(); + + if (joins != null) + { + sql = joins(sql); + } + + sql = sql + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .LeftJoin("nuEdit").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuEdit"); + + return sql; + } + + public ContentNodeKit GetMediaSource(int id) + { + var sql = MediaSourcesSelect() + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && x.NodeId == id && !x.Trashed) + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + + var dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? new ContentNodeKit() : CreateMediaNodeKit(dto); + } + + public IEnumerable GetAllMediaSources() + { + var sql = MediaSourcesSelect() + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in Database.QueryPaged(PageSize, sql)) + { + yield return CreateMediaNodeKit(row); + } + } + + public IEnumerable GetBranchMediaSources(int id) + { + var syntax = SqlSyntax; + var sql = MediaSourcesSelect( + s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) + .Where(x => x.NodeId == id, "x") + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in Database.QueryPaged(PageSize, sql)) + { + yield return CreateMediaNodeKit(row); + } + } + + public IEnumerable GetTypeMediaSources(IEnumerable ids) + { + if (!ids.Any()) + { + yield break; + } + + var sql = MediaSourcesSelect() + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) + .WhereIn(x => x.ContentTypeId, ids) + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in Database.QueryPaged(PageSize, sql)) + { + yield return CreateMediaNodeKit(row); + } + } + + private ContentNodeKit CreateContentNodeKit(ContentSourceDto dto) + { + ContentData d = null; + ContentData p = null; + + if (dto.Edited) + { + if (dto.EditData == null) + { + if (Debugger.IsAttached) + { + throw new InvalidOperationException("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding."); + } + + _logger.LogWarning("Missing cmsContentNu edited content for node {NodeId}, consider rebuilding.", dto.Id); + } + else + { + var nested = DeserializeNestedData(dto.EditData); + + d = new ContentData + { + Name = dto.EditName, + Published = false, + TemplateId = dto.EditTemplateId, + VersionId = dto.VersionId, + VersionDate = dto.EditVersionDate, + WriterId = dto.EditWriterId, + Properties = nested.PropertyData, + CultureInfos = nested.CultureData, + UrlSegment = nested.UrlSegment + }; + } + } + + if (dto.Published) + { + if (dto.PubData == null) + { + if (Debugger.IsAttached) + { + throw new InvalidOperationException("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding."); + } + + _logger.LogWarning("Missing cmsContentNu published content for node {NodeId}, consider rebuilding.", dto.Id); + } + else + { + var nested = DeserializeNestedData(dto.PubData); + + p = new ContentData + { + Name = dto.PubName, + UrlSegment = nested.UrlSegment, + Published = true, + TemplateId = dto.PubTemplateId, + VersionId = dto.VersionId, + VersionDate = dto.PubVersionDate, + WriterId = dto.PubWriterId, + Properties = nested.PropertyData, + CultureInfos = nested.CultureData + }; + } + } + + var n = new ContentNode(dto.Id, dto.Uid, + dto.Level, dto.Path, dto.SortOrder, dto.ParentId, dto.CreateDate, dto.CreatorId); + + var s = new ContentNodeKit + { + Node = n, + ContentTypeId = dto.ContentTypeId, + DraftData = d, + PublishedData = p + }; + + return s; + } + + private static ContentNodeKit CreateMediaNodeKit(ContentSourceDto dto) + { + if (dto.EditData == null) + throw new InvalidOperationException("No data for media " + dto.Id); + + var nested = DeserializeNestedData(dto.EditData); + + var p = new ContentData + { + Name = dto.EditName, + Published = true, + TemplateId = -1, + VersionId = dto.VersionId, + VersionDate = dto.EditVersionDate, + WriterId = dto.CreatorId, // what-else? + Properties = nested.PropertyData, + CultureInfos = nested.CultureData + }; + + var n = new ContentNode(dto.Id, dto.Uid, + dto.Level, dto.Path, dto.SortOrder, dto.ParentId, dto.CreateDate, dto.CreatorId); + + var s = new ContentNodeKit + { + Node = n, + ContentTypeId = dto.ContentTypeId, + PublishedData = p + }; + + return s; + } + + private static ContentNestedData DeserializeNestedData(string data) + { + // by default JsonConvert will deserialize our numeric values as Int64 + // which is bad, because they were Int32 in the database - take care + + var settings = new JsonSerializerSettings + { + Converters = new List { new ForceInt32Converter() } + }; + + return JsonConvert.DeserializeObject(data, settings); + } + } +} diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentService.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentService.cs new file mode 100644 index 0000000000..5c7fdeb53b --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentService.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using Umbraco.Core; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using Umbraco.Core.Scoping; +using Umbraco.Core.Services.Implement; +using Umbraco.Web.PublishedCache.NuCache; + +namespace Umbraco.Infrastructure.PublishedCache.Persistence +{ + public class NuCacheContentService : RepositoryService, INuCacheContentService + { + private readonly INuCacheContentRepository _repository; + + public NuCacheContentService(INuCacheContentRepository repository, IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory) + : base(provider, loggerFactory, eventMessagesFactory) + { + _repository = repository; + } + + /// + public IEnumerable GetAllContentSources() + => _repository.GetAllContentSources(); + + /// + public IEnumerable GetAllMediaSources() + => _repository.GetAllMediaSources(); + + /// + public IEnumerable GetBranchContentSources(int id) + => _repository.GetBranchContentSources(id); + + /// + public IEnumerable GetBranchMediaSources(int id) + => _repository.GetBranchMediaSources(id); + + /// + public ContentNodeKit GetContentSource(int id) + => _repository.GetContentSource(id); + + /// + public ContentNodeKit GetMediaSource(int id) + => _repository.GetMediaSource(id); + + /// + public IEnumerable GetTypeContentSources(IEnumerable ids) + => _repository.GetTypeContentSources(ids); + + /// + public IEnumerable GetTypeMediaSources(IEnumerable ids) + => _repository.GetTypeContentSources(ids); + + /// + public void DeleteContentItem(IContentBase item) + => _repository.DeleteContentItem(item); + + /// + public void RefreshContent(IContent content) + => _repository.RefreshContent(content); + + /// + public void RefreshEntity(IContentBase content) + => _repository.RefreshEntity(content); + + /// + public void Rebuild( + int groupSize = 5000, + IReadOnlyCollection contentTypeIds = null, + IReadOnlyCollection mediaTypeIds = null, + IReadOnlyCollection memberTypeIds = null) + { + using (IScope scope = ScopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) + { + if (contentTypeIds != null) + { + scope.ReadLock(Constants.Locks.ContentTree); + } + + if (mediaTypeIds != null) + { + scope.ReadLock(Constants.Locks.MediaTree); + } + + if (memberTypeIds != null) + { + scope.ReadLock(Constants.Locks.MemberTree); + } + + _repository.Rebuild(groupSize, contentTypeIds, mediaTypeIds, memberTypeIds); + scope.Complete(); + } + } + + /// + public bool VerifyContentDbCache() + => _repository.VerifyContentDbCache(); + + /// + public bool VerifyMediaDbCache() + => _repository.VerifyMediaDbCache(); + + /// + public bool VerifyMemberDbCache() + => _repository.VerifyMemberDbCache(); + } +} diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs index 97e3df16a6..3011e3d655 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs @@ -4,9 +4,8 @@ using System.Globalization; using System.IO; using System.Linq; using CSharpTest.Net.Collections; -using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; +using Microsoft.Extensions.Options; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration.Models; @@ -17,15 +16,10 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Dtos; -using Umbraco.Core.Persistence.Repositories; -using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; -using Umbraco.Core.Services.Implement; -using Umbraco.Core.Strings; -using Umbraco.Net; +using Umbraco.Infrastructure.PublishedCache.Persistence; using Umbraco.Web.Cache; using Umbraco.Web.PublishedCache.NuCache.DataSource; using Umbraco.Web.Routing; @@ -33,38 +27,30 @@ using File = System.IO.File; namespace Umbraco.Web.PublishedCache.NuCache { + internal class PublishedSnapshotService : PublishedSnapshotServiceBase { - private readonly PublishedSnapshotServiceOptions _options; - private readonly IMainDom _mainDom; - private readonly IUmbracoApplicationLifetime _lifeTime; - private readonly IRuntimeState _runtime; private readonly ServiceContext _serviceContext; private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; private readonly IProfilingLogger _profilingLogger; private readonly IScopeProvider _scopeProvider; - private readonly IDataSource _dataSource; + private readonly INuCacheContentService _publishedContentService; private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; - private readonly IDocumentRepository _documentRepository; - private readonly IMediaRepository _mediaRepository; - private readonly IMemberRepository _memberRepository; private readonly GlobalSettings _globalSettings; private readonly IEntityXmlSerializer _entitySerializer; private readonly IPublishedModelFactory _publishedModelFactory; private readonly IDefaultCultureAccessor _defaultCultureAccessor; - private readonly UrlSegmentProviderCollection _urlSegmentProviders; private readonly IHostingEnvironment _hostingEnvironment; - private readonly IShortStringHelper _shortStringHelper; private readonly IIOHelper _ioHelper; private readonly NuCacheSettings _config; // volatile because we read it with no lock private volatile bool _isReady; - private ContentStore _contentStore; - private ContentStore _mediaStore; - private SnapDictionary _domainStore; + private readonly ContentStore _contentStore; + private readonly ContentStore _mediaStore; + private readonly SnapDictionary _domainStore; private readonly object _storesLock = new object(); private readonly object _elementsLock = new object(); @@ -73,19 +59,20 @@ namespace Umbraco.Web.PublishedCache.NuCache private bool _localContentDbExists; private bool _localMediaDbExists; + private long _contentGen; + private long _mediaGen; + private long _domainGen; + private IAppCache _elementsCache; + // define constant - determines whether to use cache when previewing // to store eg routes, property converted values, anything - caching // means faster execution, but uses memory - not sure if we want it // so making it configurable. public static readonly bool FullCacheWhenPreviewing = true; - #region Constructors - public PublishedSnapshotService( PublishedSnapshotServiceOptions options, IMainDom mainDom, - IUmbracoApplicationLifetime lifeTime, - IRuntimeState runtime, ServiceContext serviceContext, IPublishedContentTypeFactory publishedContentTypeFactory, IPublishedSnapshotAccessor publishedSnapshotAccessor, @@ -93,40 +80,26 @@ namespace Umbraco.Web.PublishedCache.NuCache IProfilingLogger profilingLogger, ILoggerFactory loggerFactory, IScopeProvider scopeProvider, - IDocumentRepository documentRepository, - IMediaRepository mediaRepository, - IMemberRepository memberRepository, + INuCacheContentService publishedContentService, IDefaultCultureAccessor defaultCultureAccessor, - IDataSource dataSource, IOptions globalSettings, IEntityXmlSerializer entitySerializer, IPublishedModelFactory publishedModelFactory, - UrlSegmentProviderCollection urlSegmentProviders, IHostingEnvironment hostingEnvironment, - IShortStringHelper shortStringHelper, - IIOHelper ioHelper, + IIOHelper ioHelper, // TODO: Remove this, it is only needed for "EnsureEnvironment" which doesn't need to belong to this service IOptions config) : base(publishedSnapshotAccessor, variationContextAccessor) { - _options = options; - _mainDom = mainDom; - _lifeTime = lifeTime; - _runtime = runtime; _serviceContext = serviceContext; _publishedContentTypeFactory = publishedContentTypeFactory; _profilingLogger = profilingLogger; - _dataSource = dataSource; _loggerFactory = loggerFactory; _logger = _loggerFactory.CreateLogger(); _scopeProvider = scopeProvider; - _documentRepository = documentRepository; - _mediaRepository = mediaRepository; - _memberRepository = memberRepository; + _publishedContentService = publishedContentService; _defaultCultureAccessor = defaultCultureAccessor; _globalSettings = globalSettings.Value; - _urlSegmentProviders = urlSegmentProviders; _hostingEnvironment = hostingEnvironment; - _shortStringHelper = shortStringHelper; _ioHelper = ioHelper; _config = config.Value; @@ -135,33 +108,15 @@ namespace Umbraco.Web.PublishedCache.NuCache _entitySerializer = entitySerializer; _publishedModelFactory = publishedModelFactory; - // we always want to handle repository events, configured or not - // assuming no repository event will trigger before the whole db is ready - // (ideally we'd have Upgrading.App vs Upgrading.Data application states...) - InitializeRepositoryEvents(); - - _lifeTime.ApplicationInit += OnApplicationInit; - } - - internal void OnApplicationInit(object sender, EventArgs e) - { - // however, the cache is NOT available until we are configured, because loading - // content (and content types) from database cannot be consistent (see notes in "Handle - // Notifications" region), so - // - notifications will be ignored - // - trying to obtain a published snapshot from the service will throw - if (_runtime.Level != RuntimeLevel.Run) - return; - // lock this entire call, we only want a single thread to be accessing the stores at once and within // the call below to mainDom.Register, a callback may occur on a threadpool thread to MainDomRelease // at the same time as we are trying to write to the stores. MainDomRelease also locks on _storesLock so // it will not be able to close the stores until we are done populating (if the store is empty) lock (_storesLock) { - if (!_options.IgnoreLocalDb) + if (!options.IgnoreLocalDb) { - _mainDom.Register(MainDomRegister, MainDomRelease); + mainDom.Register(MainDomRegister, MainDomRelease); // stores are created with a db so they can write to it, but they do not read from it, // stores need to be populated, happens in OnResolutionFrozen which uses _localDbExists to @@ -181,8 +136,6 @@ namespace Umbraco.Web.PublishedCache.NuCache } _domainStore = new SnapDictionary(); - - LoadCachesOnStartup(); } } @@ -190,6 +143,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // NOTE: These aren't used within this object but are made available internally to improve the IdKey lookup performance // when nucache is enabled. + // TODO: Does this need to be here? internal int GetDocumentId(Guid udi) => GetId(_contentStore, udi); internal int GetMediaId(Guid udi) => GetId(_mediaStore, udi); @@ -239,11 +193,11 @@ namespace Umbraco.Web.PublishedCache.NuCache lock (_storesLock) { _logger.LogDebug("Releasing content store..."); - _contentStore?.ReleaseLocalDb(); //null check because we could shut down before being assigned + _contentStore?.ReleaseLocalDb(); // null check because we could shut down before being assigned _localContentDb = null; _logger.LogDebug("Releasing media store..."); - _mediaStore?.ReleaseLocalDb(); //null check because we could shut down before being assigned + _mediaStore?.ReleaseLocalDb(); // null check because we could shut down before being assigned _localMediaDb = null; _logger.LogInformation("Released from MainDom"); @@ -253,100 +207,61 @@ namespace Umbraco.Web.PublishedCache.NuCache /// /// Populates the stores /// - /// This is called inside of a lock for _storesLock - private void LoadCachesOnStartup() + public override void LoadCachesOnStartup() { - var okContent = false; - var okMedia = false; - - try + lock (_storesLock) { - if (_localContentDbExists) + if (_isReady) { - okContent = LockAndLoadContent(scope => LoadContentFromLocalDbLocked(true)); + throw new InvalidOperationException("The caches can only be loaded on startup one time"); + } + + var okContent = false; + var okMedia = false; + + try + { + if (_localContentDbExists) + { + okContent = LockAndLoadContent(() => LoadContentFromLocalDbLocked(true)); + if (!okContent) + { + _logger.LogWarning("Loading content from local db raised warnings, will reload from database."); + } + } + + if (_localMediaDbExists) + { + okMedia = LockAndLoadMedia(() => LoadMediaFromLocalDbLocked(true)); + if (!okMedia) + { + _logger.LogWarning("Loading media from local db raised warnings, will reload from database."); + } + } + if (!okContent) - _logger.LogWarning("Loading content from local db raised warnings, will reload from database."); - } + { + LockAndLoadContent(() => LoadContentFromDatabaseLocked(true)); + } - if (_localMediaDbExists) - { - okMedia = LockAndLoadMedia(scope => LoadMediaFromLocalDbLocked(true)); if (!okMedia) - _logger.LogWarning("Loading media from local db raised warnings, will reload from database."); + { + LockAndLoadMedia(() => LoadMediaFromDatabaseLocked(true)); + } + + LockAndLoadDomains(); + } + catch (Exception ex) + { + _logger.LogCritical(ex, "Panic, exception while loading cache data."); + throw; } - if (!okContent) - LockAndLoadContent(scope => LoadContentFromDatabaseLocked(scope, true)); - - if (!okMedia) - LockAndLoadMedia(scope => LoadMediaFromDatabaseLocked(scope, true)); - - LockAndLoadDomains(); + // finally, cache is ready! + _isReady = true; } - catch (Exception ex) - { - _logger.LogCritical(ex, "Panic, exception while loading cache data."); - throw; - } - - // finally, cache is ready! - _isReady = true; } - private void InitializeRepositoryEvents() - { - // TODO: The reason these events are in the repository is for legacy, the events should exist at the service - // level now since we can fire these events within the transaction... so move the events to service level - - // plug repository event handlers - // these trigger within the transaction to ensure consistency - // and are used to maintain the central, database-level XML cache - DocumentRepository.ScopeEntityRemove += OnContentRemovingEntity; - //ContentRepository.RemovedVersion += OnContentRemovedVersion; - DocumentRepository.ScopedEntityRefresh += OnContentRefreshedEntity; - MediaRepository.ScopeEntityRemove += OnMediaRemovingEntity; - //MediaRepository.RemovedVersion += OnMediaRemovedVersion; - MediaRepository.ScopedEntityRefresh += OnMediaRefreshedEntity; - MemberRepository.ScopeEntityRemove += OnMemberRemovingEntity; - //MemberRepository.RemovedVersion += OnMemberRemovedVersion; - MemberRepository.ScopedEntityRefresh += OnMemberRefreshedEntity; - - // plug - ContentTypeService.ScopedRefreshedEntity += OnContentTypeRefreshedEntity; - MediaTypeService.ScopedRefreshedEntity += OnMediaTypeRefreshedEntity; - MemberTypeService.ScopedRefreshedEntity += OnMemberTypeRefreshedEntity; - - LocalizationService.SavedLanguage += OnLanguageSaved; - } - - private void TearDownRepositoryEvents() - { - DocumentRepository.ScopeEntityRemove -= OnContentRemovingEntity; - //ContentRepository.RemovedVersion -= OnContentRemovedVersion; - DocumentRepository.ScopedEntityRefresh -= OnContentRefreshedEntity; - MediaRepository.ScopeEntityRemove -= OnMediaRemovingEntity; - //MediaRepository.RemovedVersion -= OnMediaRemovedVersion; - MediaRepository.ScopedEntityRefresh -= OnMediaRefreshedEntity; - MemberRepository.ScopeEntityRemove -= OnMemberRemovingEntity; - //MemberRepository.RemovedVersion -= OnMemberRemovedVersion; - MemberRepository.ScopedEntityRefresh -= OnMemberRefreshedEntity; - - ContentTypeService.ScopedRefreshedEntity -= OnContentTypeRefreshedEntity; - MediaTypeService.ScopedRefreshedEntity -= OnMediaTypeRefreshedEntity; - MemberTypeService.ScopedRefreshedEntity -= OnMemberTypeRefreshedEntity; - - LocalizationService.SavedLanguage -= OnLanguageSaved; - } - - public override void Dispose() - { - TearDownRepositoryEvents(); - _lifeTime.ApplicationInit -= OnApplicationInit; - base.Dispose(); - } - - #endregion - #region Local files private string GetLocalFilesPath() @@ -354,7 +269,9 @@ namespace Umbraco.Web.PublishedCache.NuCache var path = Path.Combine(_hostingEnvironment.LocalTempPath, "NuCache"); if (!Directory.Exists(path)) + { Directory.CreateDirectory(path); + } return path; } @@ -362,23 +279,31 @@ namespace Umbraco.Web.PublishedCache.NuCache private void DeleteLocalFilesForContent() { if (_isReady && _localContentDb != null) + { throw new InvalidOperationException("Cannot delete local files while the cache uses them."); + } var path = GetLocalFilesPath(); var localContentDbPath = Path.Combine(path, "NuCache.Content.db"); if (File.Exists(localContentDbPath)) + { File.Delete(localContentDbPath); + } } private void DeleteLocalFilesForMedia() { if (_isReady && _localMediaDb != null) + { throw new InvalidOperationException("Cannot delete local files while the cache uses them."); + } var path = GetLocalFilesPath(); var localMediaDbPath = Path.Combine(path, "NuCache.Media.db"); if (File.Exists(localMediaDbPath)) + { File.Delete(localMediaDbPath); + } } #endregion @@ -400,11 +325,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // sudden panic... but in RepeatableRead can a content that I haven't already read, be removed // before I read it? NO! because the WHOLE content tree is read-locked using WithReadLocked. // don't panic. - - private bool LockAndLoadContent(Func action) + private bool LockAndLoadContent(Func action) { - - // first get a writer, then a scope // if there already is a scope, the writer will attach to it // otherwise, it will only exist here - cheap @@ -412,13 +334,13 @@ namespace Umbraco.Web.PublishedCache.NuCache using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.ContentTree); - var ok = action(scope); + var ok = action(); scope.Complete(); return ok; } } - private bool LoadContentFromDatabaseLocked(IScope scope, bool onStartup) + private bool LoadContentFromDatabaseLocked(bool onStartup) { // locks: // contentStore is wlocked (1 thread) @@ -437,7 +359,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _localContentDb?.Clear(); // IMPORTANT GetAllContentSources sorts kits by level + parentId + sortOrder - var kits = _dataSource.GetAllContentSources(scope); + var kits = _publishedContentService.GetAllContentSources(); return onStartup ? _contentStore.SetAllFastSortedLocked(kits, true) : _contentStore.SetAllLocked(kits); } } @@ -457,45 +379,22 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - // keep these around - might be useful - - //private void LoadContentBranch(IContent content) - //{ - // LoadContent(content); - - // foreach (var child in content.Children()) - // LoadContentBranch(child); - //} - - //private void LoadContent(IContent content) - //{ - // var contentService = _serviceContext.ContentService as ContentService; - // var newest = content; - // var published = newest.Published - // ? newest - // : (newest.HasPublishedVersion ? contentService.GetByVersion(newest.PublishedVersionGuid) : null); - - // var contentNode = CreateContentNode(newest, published); - // _contentStore.Set(contentNode); - //} - - private bool LockAndLoadMedia(Func action) + private bool LockAndLoadMedia(Func action) { // see note in LockAndLoadContent using (_mediaStore.GetScopedWriteLock(_scopeProvider)) using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.MediaTree); - var ok = action(scope); + var ok = action(); scope.Complete(); return ok; } } - private bool LoadMediaFromDatabaseLocked(IScope scope, bool onStartup) + private bool LoadMediaFromDatabaseLocked(bool onStartup) { // locks & notes: see content - var mediaTypes = _serviceContext.MediaTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); _mediaStore.SetAllContentTypesLocked(mediaTypes); @@ -504,12 +403,11 @@ namespace Umbraco.Web.PublishedCache.NuCache { // beware! at that point the cache is inconsistent, // assuming we are going to SetAll content items! - _localMediaDb?.Clear(); _logger.LogDebug("Loading media from database..."); // IMPORTANT GetAllMediaSources sorts kits by level + parentId + sortOrder - var kits = _dataSource.GetAllMediaSources(scope); + var kits = _publishedContentService.GetAllMediaSources(); return onStartup ? _mediaStore.SetAllFastSortedLocked(kits, true) : _mediaStore.SetAllLocked(kits); } } @@ -527,7 +425,6 @@ namespace Umbraco.Web.PublishedCache.NuCache return LoadEntitiesFromLocalDbLocked(onStartup, _localMediaDb, _mediaStore, "media"); } - } private bool LoadEntitiesFromLocalDbLocked(bool onStartup, BPlusTree localDb, ContentStore store, string entityType) @@ -562,90 +459,6 @@ namespace Umbraco.Web.PublishedCache.NuCache return onStartup ? store.SetAllFastSortedLocked(kits, false) : store.SetAllLocked(kits); } - // keep these around - might be useful - - //private void LoadMediaBranch(IMedia media) - //{ - // LoadMedia(media); - - // foreach (var child in media.Children()) - // LoadMediaBranch(child); - //} - - //private void LoadMedia(IMedia media) - //{ - // var mediaType = _contentTypeCache.Get(PublishedItemType.Media, media.ContentTypeId); - - // var mediaData = new ContentData - // { - // Name = media.Name, - // Published = true, - // Version = media.Version, - // VersionDate = media.UpdateDate, - // WriterId = media.CreatorId, // what else? - // TemplateId = -1, // have none - // Properties = GetPropertyValues(media) - // }; - - // var mediaNode = new ContentNode(media.Id, mediaType, - // media.Level, media.Path, media.SortOrder, - // media.ParentId, media.CreateDate, media.CreatorId, - // null, mediaData); - - // _mediaStore.Set(mediaNode); - //} - - //private Dictionary GetPropertyValues(IContentBase content) - //{ - // var propertyEditorResolver = PropertyEditorResolver.Current; // should inject - - // return content - // .Properties - // .Select(property => - // { - // var e = propertyEditorResolver.GetByAlias(property.PropertyType.PropertyEditorAlias); - // var v = e == null - // ? property.Value - // : e.ValueEditor.ConvertDbToString(property, property.PropertyType, _serviceContext.DataTypeService); - // return new KeyValuePair(property.Alias, v); - // }) - // .ToDictionary(x => x.Key, x => x.Value); - //} - - //private ContentData CreateContentData(IContent content) - //{ - // return new ContentData - // { - // Name = content.Name, - // Published = content.Published, - // Version = content.Version, - // VersionDate = content.UpdateDate, - // WriterId = content.WriterId, - // TemplateId = content.Template == null ? -1 : content.Template.Id, - // Properties = GetPropertyValues(content) - // }; - //} - - //private ContentNode CreateContentNode(IContent newest, IContent published) - //{ - // var contentType = _contentTypeCache.Get(PublishedItemType.Content, newest.ContentTypeId); - - // var draftData = newest.Published - // ? null - // : CreateContentData(newest); - - // var publishedData = newest.Published - // ? CreateContentData(newest) - // : (published == null ? null : CreateContentData(published)); - - // var contentNode = new ContentNode(newest.Id, contentType, - // newest.Level, newest.Path, newest.SortOrder, - // newest.ParentId, newest.CreateDate, newest.CreatorId, - // draftData, publishedData); - - // return contentNode; - //} - private void LockAndLoadDomains() { // see note in LockAndLoadContent @@ -713,9 +526,10 @@ namespace Umbraco.Web.PublishedCache.NuCache publishedChanged = publishedChanged2; } - if (draftChanged || publishedChanged) + { ((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync(); + } } // Calling this method means we have a lock on the contentStore (i.e. GetScopedWriteLock) @@ -739,7 +553,7 @@ namespace Umbraco.Web.PublishedCache.NuCache using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.ContentTree); - LoadContentFromDatabaseLocked(scope, false); + LoadContentFromDatabaseLocked(false); scope.Complete(); } draftChanged = publishedChanged = true; @@ -770,13 +584,13 @@ namespace Umbraco.Web.PublishedCache.NuCache { // ?? should we do some RV check here? // IMPORTANT GetbranchContentSources sorts kits by level and by sort order - var kits = _dataSource.GetBranchContentSources(scope, capture.Id); + var kits = _publishedContentService.GetBranchContentSources(capture.Id); _contentStore.SetBranchLocked(capture.Id, kits); } else { // ?? should we do some RV check here? - var kit = _dataSource.GetContentSource(scope, capture.Id); + var kit = _publishedContentService.GetContentSource(capture.Id); if (kit.IsEmpty) { _contentStore.ClearLocked(capture.Id); @@ -813,7 +627,9 @@ namespace Umbraco.Web.PublishedCache.NuCache } if (anythingChanged) + { ((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync(); + } } private void NotifyLocked(IEnumerable payloads, out bool anythingChanged) @@ -832,9 +648,10 @@ namespace Umbraco.Web.PublishedCache.NuCache using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.MediaTree); - LoadMediaFromDatabaseLocked(scope, false); + LoadMediaFromDatabaseLocked(false); scope.Complete(); } + anythingChanged = true; continue; } @@ -842,7 +659,10 @@ namespace Umbraco.Web.PublishedCache.NuCache if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) { if (_mediaStore.ClearLocked(payload.Id)) + { anythingChanged = true; + } + continue; } @@ -853,7 +673,6 @@ namespace Umbraco.Web.PublishedCache.NuCache } // TODO: should we do some RV checks here? (later) - var capture = payload; using (var scope = _scopeProvider.CreateScope()) { @@ -863,13 +682,13 @@ namespace Umbraco.Web.PublishedCache.NuCache { // ?? should we do some RV check here? // IMPORTANT GetbranchContentSources sorts kits by level and by sort order - var kits = _dataSource.GetBranchMediaSources(scope, capture.Id); + var kits = _publishedContentService.GetBranchMediaSources(capture.Id); _mediaStore.SetBranchLocked(capture.Id, kits); } else { // ?? should we do some RV check here? - var kit = _dataSource.GetMediaSource(scope, capture.Id); + var kit = _publishedContentService.GetMediaSource(capture.Id); if (kit.IsEmpty) { _mediaStore.ClearLocked(capture.Id); @@ -893,22 +712,26 @@ namespace Umbraco.Web.PublishedCache.NuCache { // no cache, nothing we can do if (_isReady == false) + { return; + } foreach (var payload in payloads) + { _logger.LogDebug("Notified {ChangeTypes} for {ItemType} {ItemId}", payload.ChangeTypes, payload.ItemType, payload.Id); + } Notify(_contentStore, payloads, RefreshContentTypesLocked); Notify(_mediaStore, payloads, RefreshMediaTypesLocked); if (_publishedModelFactory.IsLiveFactoryEnabled()) { - //In the case of Pure Live - we actually need to refresh all of the content and the media - //see https://github.com/umbraco/Umbraco-CMS/issues/5671 - //The underlying issue is that in Pure Live the ILivePublishedModelFactory will re-compile all of the classes/models - //into a new DLL for the application which includes both content types and media types. - //Since the models in the cache are based on these actual classes, all of the objects in the cache need to be updated - //to use the newest version of the class. + // In the case of Pure Live - we actually need to refresh all of the content and the media + // see https://github.com/umbraco/Umbraco-CMS/issues/5671 + // The underlying issue is that in Pure Live the ILivePublishedModelFactory will re-compile all of the classes/models + // into a new DLL for the application which includes both content types and media types. + // Since the models in the cache are based on these actual classes, all of the objects in the cache need to be updated + // to use the newest version of the class. // NOTE: Ideally this can be run on background threads here which would prevent blocking the UI // as is the case when saving a content type. Intially one would think that it won't be any different @@ -941,7 +764,10 @@ namespace Umbraco.Web.PublishedCache.NuCache private void Notify(ContentStore store, ContentTypeCacheRefresher.JsonPayload[] payloads, Action, List, List, List> action) where T : IContentTypeComposition { - if (payloads.Length == 0) return; //nothing to do + if (payloads.Length == 0) + { + return; // nothing to do + } var nameOfT = typeof(T).Name; @@ -949,25 +775,37 @@ namespace Umbraco.Web.PublishedCache.NuCache foreach (var payload in payloads) { - if (payload.ItemType != nameOfT) continue; + if (payload.ItemType != nameOfT) + { + continue; + } if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.Remove)) + { AddToList(ref removedIds, payload.Id); + } else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshMain)) + { AddToList(ref refreshedIds, payload.Id); + } else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshOther)) + { AddToList(ref otherIds, payload.Id); + } else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.Create)) + { AddToList(ref newIds, payload.Id); + } } - if (removedIds.IsCollectionEmpty() && refreshedIds.IsCollectionEmpty() && otherIds.IsCollectionEmpty() && newIds.IsCollectionEmpty()) return; + if (removedIds.IsCollectionEmpty() && refreshedIds.IsCollectionEmpty() && otherIds.IsCollectionEmpty() && newIds.IsCollectionEmpty()) + { + return; + } using (store.GetScopedWriteLock(_scopeProvider)) { - // ReSharper disable AccessToModifiedClosure action(removedIds, refreshedIds, otherIds, newIds); - // ReSharper restore AccessToModifiedClosure } } @@ -975,14 +813,18 @@ namespace Umbraco.Web.PublishedCache.NuCache { // no cache, nothing we can do if (_isReady == false) + { return; + } var idsA = payloads.Select(x => x.Id).ToArray(); foreach (var payload in payloads) + { _logger.LogDebug("Notified {RemovedStatus} for data type {DataTypeId}", payload.Removed ? "Removed" : "Refreshed", payload.Id); + } using (_contentStore.GetScopedWriteLock(_scopeProvider)) using (_mediaStore.GetScopedWriteLock(_scopeProvider)) @@ -1015,7 +857,9 @@ namespace Umbraco.Web.PublishedCache.NuCache { // no cache, nothing we can do if (_isReady == false) + { return; + } // see note in LockAndLoadContent using (_domainStore.GetScopedWriteLock(_scopeProvider)) @@ -1048,7 +892,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - //Methods used to prevent allocations of lists + // Methods used to prevent allocations of lists private void AddToList(ref List list, int val) => GetOrCreateList(ref list).Add(val); private List GetOrCreateList(ref List list) => list ?? (list = new List()); @@ -1060,7 +904,9 @@ namespace Umbraco.Web.PublishedCache.NuCache { // XxxTypeService.GetAll(empty) returns everything! if (ids.Length == 0) + { return Array.Empty(); + } IEnumerable contentTypes; switch (itemType) @@ -1107,7 +953,9 @@ namespace Umbraco.Web.PublishedCache.NuCache private void RefreshContentTypesLocked(List removedIds, List refreshedIds, List otherIds, List newIds) { if (removedIds.IsCollectionEmpty() && refreshedIds.IsCollectionEmpty() && otherIds.IsCollectionEmpty() && newIds.IsCollectionEmpty()) + { return; + } // locks: // content (and content types) are read-locked while reading content @@ -1124,13 +972,19 @@ namespace Umbraco.Web.PublishedCache.NuCache var kits = refreshedIds.IsCollectionEmpty() ? Array.Empty() - : _dataSource.GetTypeContentSources(scope, refreshedIds).ToArray(); + : _publishedContentService.GetTypeContentSources(refreshedIds).ToArray(); _contentStore.UpdateContentTypesLocked(removedIds, typesA, kits); if (!otherIds.IsCollectionEmpty()) + { _contentStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Content, otherIds.ToArray())); + } + if (!newIds.IsCollectionEmpty()) + { _contentStore.NewContentTypesLocked(CreateContentTypes(PublishedItemType.Content, newIds.ToArray())); + } + scope.Complete(); } } @@ -1138,7 +992,9 @@ namespace Umbraco.Web.PublishedCache.NuCache private void RefreshMediaTypesLocked(List removedIds, List refreshedIds, List otherIds, List newIds) { if (removedIds.IsCollectionEmpty() && refreshedIds.IsCollectionEmpty() && otherIds.IsCollectionEmpty() && newIds.IsCollectionEmpty()) + { return; + } // locks: // media (and content types) are read-locked while reading media @@ -1155,13 +1011,19 @@ namespace Umbraco.Web.PublishedCache.NuCache var kits = refreshedIds == null ? Array.Empty() - : _dataSource.GetTypeMediaSources(scope, refreshedIds).ToArray(); + : _publishedContentService.GetTypeMediaSources(refreshedIds).ToArray(); _mediaStore.UpdateContentTypesLocked(removedIds, typesA, kits); if (!otherIds.IsCollectionEmpty()) + { _mediaStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); + } + if (!newIds.IsCollectionEmpty()) + { _mediaStore.NewContentTypesLocked(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); + } + scope.Complete(); } } @@ -1170,14 +1032,13 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Create, Get Published Snapshot - private long _contentGen, _mediaGen, _domainGen; - private IAppCache _elementsCache; - public override IPublishedSnapshot CreatePublishedSnapshot(string previewToken) { // no cache, no joy if (_isReady == false) + { throw new InvalidOperationException("The published snapshot service has not properly initialized."); + } var preview = previewToken.IsNullOrWhiteSpace() == false; return new PublishedSnapshot(this, preview); @@ -1272,6 +1133,7 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Preview + // TODO: Delete this all public override string EnterPreview(IUser user, int contentId) { return "preview"; // anything @@ -1289,522 +1151,52 @@ namespace Umbraco.Web.PublishedCache.NuCache #endregion - #region Handle Repository Events For Database PreCache - - // note: if the service is not ready, ie _isReady is false, then we still handle repository events, - // because we can, we do not need a working published snapshot to do it - the only reason why it could cause an - // issue is if the database table is not ready, but that should be prevented by migrations. - - // we need them to be "repository" events ie to trigger from within the repository transaction, - // because they need to be consistent with the content that is being refreshed/removed - and that - // should be guaranteed by a DB transaction - - private void OnContentRemovingEntity(DocumentRepository sender, DocumentRepository.ScopedEntityEventArgs args) - { - OnRemovedEntity(args.Scope.Database, args.Entity); - } - - private void OnMediaRemovingEntity(MediaRepository sender, MediaRepository.ScopedEntityEventArgs args) - { - OnRemovedEntity(args.Scope.Database, args.Entity); - } - - private void OnMemberRemovingEntity(MemberRepository sender, MemberRepository.ScopedEntityEventArgs args) - { - OnRemovedEntity(args.Scope.Database, args.Entity); - } - - private void OnRemovedEntity(IUmbracoDatabase db, IContentBase item) - { - db.Execute("DELETE FROM cmsContentNu WHERE nodeId=@id", new { id = item.Id }); - } - - private void OnContentRefreshedEntity(DocumentRepository sender, DocumentRepository.ScopedEntityEventArgs args) - { - var db = args.Scope.Database; - var content = (Content)args.Entity; - - // always refresh the edited data - OnRepositoryRefreshed(db, content, false); - - // if unpublishing, remove published data from table - if (content.PublishedState == PublishedState.Unpublishing) - db.Execute("DELETE FROM cmsContentNu WHERE nodeId=@id AND published=1", new { id = content.Id }); - - // if publishing, refresh the published data - else if (content.PublishedState == PublishedState.Publishing) - OnRepositoryRefreshed(db, content, true); - } - - private void OnMediaRefreshedEntity(MediaRepository sender, MediaRepository.ScopedEntityEventArgs args) - { - var db = args.Scope.Database; - var media = args.Entity; - - // refresh the edited data - OnRepositoryRefreshed(db, media, false); - } - - private void OnMemberRefreshedEntity(MemberRepository sender, MemberRepository.ScopedEntityEventArgs args) - { - var db = args.Scope.Database; - var member = args.Entity; - - // refresh the edited data - OnRepositoryRefreshed(db, member, false); - } - - private void OnRepositoryRefreshed(IUmbracoDatabase db, IContentBase content, bool published) - { - // use a custom SQL to update row version on each update - //db.InsertOrUpdate(dto); - - var dto = GetDto(content, published); - db.InsertOrUpdate(dto, - "SET data=@data, rv=rv+1 WHERE nodeId=@id AND published=@published", - new - { - data = dto.Data, - id = dto.NodeId, - published = dto.Published - }); - } - - private void OnContentTypeRefreshedEntity(IContentTypeService sender, ContentTypeChange.EventArgs args) - { - const ContentTypeChangeTypes types // only for those that have been refreshed - = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther; - var contentTypeIds = args.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id).ToArray(); - if (contentTypeIds.Any()) - RebuildContentDbCache(contentTypeIds: contentTypeIds); - } - - private void OnMediaTypeRefreshedEntity(IMediaTypeService sender, ContentTypeChange.EventArgs args) - { - const ContentTypeChangeTypes types // only for those that have been refreshed - = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther; - var mediaTypeIds = args.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id).ToArray(); - if (mediaTypeIds.Any()) - RebuildMediaDbCache(contentTypeIds: mediaTypeIds); - } - - private void OnMemberTypeRefreshedEntity(IMemberTypeService sender, ContentTypeChange.EventArgs args) - { - const ContentTypeChangeTypes types // only for those that have been refreshed - = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther; - var memberTypeIds = args.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id).ToArray(); - if (memberTypeIds.Any()) - RebuildMemberDbCache(contentTypeIds: memberTypeIds); - } - - /// - /// If a is ever saved with a different culture, we need to rebuild all of the content nucache table - /// - /// - /// - private void OnLanguageSaved(ILocalizationService sender, Core.Events.SaveEventArgs e) - { - //culture changed on an existing language - var cultureChanged = e.SavedEntities.Any(x => !x.WasPropertyDirty(nameof(ILanguage.Id)) && x.WasPropertyDirty(nameof(ILanguage.IsoCode))); - if (cultureChanged) - { - RebuildContentDbCache(); - } - } - - private ContentNuDto GetDto(IContentBase content, bool published) - { - // should inject these in ctor - // BUT for the time being we decide not to support ConvertDbToXml/String - //var propertyEditorResolver = PropertyEditorResolver.Current; - //var dataTypeService = ApplicationContext.Current.Services.DataTypeService; - - var propertyData = new Dictionary(); - foreach (var prop in content.Properties) - { - var pdatas = new List(); - foreach (var pvalue in prop.Values) - { - // sanitize - properties should be ok but ... never knows - if (!prop.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) - continue; - - // note: at service level, invariant is 'null', but here invariant becomes 'string.Empty' - var value = published ? pvalue.PublishedValue : pvalue.EditedValue; - if (value != null) - pdatas.Add(new PropertyData { Culture = pvalue.Culture ?? string.Empty, Segment = pvalue.Segment ?? string.Empty, Value = value }); - - //Core.Composing.Current.Logger.Debug($"{content.Id} {prop.Alias} [{pvalue.LanguageId},{pvalue.Segment}] {value} {(published?"pub":"edit")}"); - - //if (value != null) - //{ - // var e = propertyEditorResolver.GetByAlias(prop.PropertyType.PropertyEditorAlias); - - // // We are converting to string, even for database values which are integer or - // // DateTime, which is not optimum. Doing differently would require that we have a way to tell - // // whether the conversion to XML string changes something or not... which we don't, and we - // // don't want to implement it as PropertyValueEditor.ConvertDbToXml/String should die anyway. - - // // Don't think about improving the situation here: this is a corner case and the real - // // thing to do is to get rig of PropertyValueEditor.ConvertDbToXml/String. - - // // Use ConvertDbToString to keep it simple, although everywhere we use ConvertDbToXml and - // // nothing ensures that the two methods are consistent. - - // if (e != null) - // value = e.ValueEditor.ConvertDbToString(prop, prop.PropertyType, dataTypeService); - //} - } - propertyData[prop.Alias] = pdatas.ToArray(); - } - - var cultureData = new Dictionary(); - - // sanitize - names should be ok but ... never knows - if (content.ContentType.VariesByCulture()) - { - var infos = content is IContent document - ? (published - ? document.PublishCultureInfos - : document.CultureInfos) - : content.CultureInfos; - - // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in infos) - { - var cultureIsDraft = !published && content is IContent d && d.IsCultureEdited(cultureInfo.Culture); - cultureData[cultureInfo.Culture] = new CultureVariation - { - Name = cultureInfo.Name, - UrlSegment = content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders, cultureInfo.Culture), - Date = content.GetUpdateDate(cultureInfo.Culture) ?? DateTime.MinValue, - IsDraft = cultureIsDraft - }; - } - } - - //the dictionary that will be serialized - var nestedData = new ContentNestedData - { - PropertyData = propertyData, - CultureData = cultureData, - UrlSegment = content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders) - }; - - var dto = new ContentNuDto - { - NodeId = content.Id, - Published = published, - - // note that numeric values (which are Int32) are serialized without their - // type (eg "value":1234) and JsonConvert by default deserializes them as Int64 - - Data = JsonConvert.SerializeObject(nestedData) - }; - - //Core.Composing.Current.Logger.Debug(dto.Data); - - return dto; - } - - #endregion #region Rebuild Database PreCache - public override void Rebuild() - { - _logger.LogDebug("Rebuilding..."); - using (var scope = _scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) - { - scope.ReadLock(Constants.Locks.ContentTree); - scope.ReadLock(Constants.Locks.MediaTree); - scope.ReadLock(Constants.Locks.MemberTree); - RebuildContentDbCacheLocked(scope, 5000, null); - RebuildMediaDbCacheLocked(scope, 5000, null); - RebuildMemberDbCacheLocked(scope, 5000, null); - scope.Complete(); - } - } - - public void RebuildContentDbCache(int groupSize = 5000, IEnumerable contentTypeIds = null) - { - using (var scope = _scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) - { - scope.ReadLock(Constants.Locks.ContentTree); - RebuildContentDbCacheLocked(scope, groupSize, contentTypeIds); - scope.Complete(); - } - } - - // assumes content tree lock - private void RebuildContentDbCacheLocked(IScope scope, int groupSize, IEnumerable contentTypeIds) - { - var contentTypeIdsA = contentTypeIds?.ToArray(); - var contentObjectType = Constants.ObjectTypes.Document; - var db = scope.Database; - - // remove all - if anything fails the transaction will rollback - if (contentTypeIds == null || contentTypeIdsA.Length == 0) - { - // must support SQL-CE - db.Execute(@"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType -)", - new { objType = contentObjectType }); - } - else - { - // assume number of ctypes won't blow IN(...) - // must support SQL-CE - db.Execute($@"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode - JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id - WHERE umbracoNode.nodeObjectType=@objType - AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes) -)", - new { objType = contentObjectType, ctypes = contentTypeIdsA }); - } - - // insert back - if anything fails the transaction will rollback - var query = scope.SqlContext.Query(); - if (contentTypeIds != null && contentTypeIdsA.Length > 0) - query = query.WhereIn(x => x.ContentTypeId, contentTypeIdsA); // assume number of ctypes won't blow IN(...) - - long pageIndex = 0; - long processed = 0; - long total; - do - { - // the tree is locked, counting and comparing to total is safe - var descendants = _documentRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); - var items = new List(); - var count = 0; - foreach (var c in descendants) - { - // always the edited version - items.Add(GetDto(c, false)); - - // and also the published version if it makes any sense - if (c.Published) - items.Add(GetDto(c, true)); - - count++; - } - - db.BulkInsertRecords(items); - processed += count; - } while (processed < total); - } - - public void RebuildMediaDbCache(int groupSize = 5000, IEnumerable contentTypeIds = null) - { - using (var scope = _scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) - { - scope.ReadLock(Constants.Locks.MediaTree); - RebuildMediaDbCacheLocked(scope, groupSize, contentTypeIds); - scope.Complete(); - } - } - - // assumes media tree lock - public void RebuildMediaDbCacheLocked(IScope scope, int groupSize, IEnumerable contentTypeIds) - { - var contentTypeIdsA = contentTypeIds?.ToArray(); - var mediaObjectType = Constants.ObjectTypes.Media; - var db = scope.Database; - - // remove all - if anything fails the transaction will rollback - if (contentTypeIds == null || contentTypeIdsA.Length == 0) - { - // must support SQL-CE - db.Execute(@"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType -)", - new { objType = mediaObjectType }); - } - else - { - // assume number of ctypes won't blow IN(...) - // must support SQL-CE - db.Execute($@"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode - JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id - WHERE umbracoNode.nodeObjectType=@objType - AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes) -)", - new { objType = mediaObjectType, ctypes = contentTypeIdsA }); - } - - // insert back - if anything fails the transaction will rollback - var query = scope.SqlContext.Query(); - if (contentTypeIds != null && contentTypeIdsA.Length > 0) - query = query.WhereIn(x => x.ContentTypeId, contentTypeIdsA); // assume number of ctypes won't blow IN(...) - - long pageIndex = 0; - long processed = 0; - long total; - do - { - // the tree is locked, counting and comparing to total is safe - var descendants = _mediaRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); - var items = descendants.Select(m => GetDto(m, false)).ToList(); - db.BulkInsertRecords(items); - processed += items.Count; - } while (processed < total); - } - - public void RebuildMemberDbCache(int groupSize = 5000, IEnumerable contentTypeIds = null) - { - using (var scope = _scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) - { - scope.ReadLock(Constants.Locks.MemberTree); - RebuildMemberDbCacheLocked(scope, groupSize, contentTypeIds); - scope.Complete(); - } - } - - // assumes member tree lock - public void RebuildMemberDbCacheLocked(IScope scope, int groupSize, IEnumerable contentTypeIds) - { - var contentTypeIdsA = contentTypeIds?.ToArray(); - var memberObjectType = Constants.ObjectTypes.Member; - var db = scope.Database; - - // remove all - if anything fails the transaction will rollback - if (contentTypeIds == null || contentTypeIdsA.Length == 0) - { - // must support SQL-CE - db.Execute(@"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType -)", - new { objType = memberObjectType }); - } - else - { - // assume number of ctypes won't blow IN(...) - // must support SQL-CE - db.Execute($@"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode - JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id - WHERE umbracoNode.nodeObjectType=@objType - AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes) -)", - new { objType = memberObjectType, ctypes = contentTypeIdsA }); - } - - // insert back - if anything fails the transaction will rollback - var query = scope.SqlContext.Query(); - if (contentTypeIds != null && contentTypeIdsA.Length > 0) - query = query.WhereIn(x => x.ContentTypeId, contentTypeIdsA); // assume number of ctypes won't blow IN(...) - - long pageIndex = 0; - long processed = 0; - long total; - do - { - var descendants = _memberRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); - var items = descendants.Select(m => GetDto(m, false)).ToArray(); - db.BulkInsertRecords(items); - processed += items.Length; - } while (processed < total); - } + public override void Rebuild( + int groupSize = 5000, + IReadOnlyCollection contentTypeIds = null, + IReadOnlyCollection mediaTypeIds = null, + IReadOnlyCollection memberTypeIds = null) + => _publishedContentService.Rebuild(groupSize, contentTypeIds, mediaTypeIds, memberTypeIds); public bool VerifyContentDbCache() { + // TODO: Shouldn't this entire logic just exist in the call to _publishedContentService? using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.ContentTree); - var ok = VerifyContentDbCacheLocked(scope); + var ok = _publishedContentService.VerifyContentDbCache(); scope.Complete(); return ok; } } - // assumes content tree lock - private bool VerifyContentDbCacheLocked(IScope scope) - { - // every document should have a corresponding row for edited properties - // and if published, may have a corresponding row for published properties - - var contentObjectType = Constants.ObjectTypes.Document; - var db = scope.Database; - - var count = db.ExecuteScalar($@"SELECT COUNT(*) -FROM umbracoNode -JOIN {Constants.DatabaseSchema.Tables.Document} ON umbracoNode.id={Constants.DatabaseSchema.Tables.Document}.nodeId -LEFT JOIN cmsContentNu nuEdited ON (umbracoNode.id=nuEdited.nodeId AND nuEdited.published=0) -LEFT JOIN cmsContentNu nuPublished ON (umbracoNode.id=nuPublished.nodeId AND nuPublished.published=1) -WHERE umbracoNode.nodeObjectType=@objType -AND nuEdited.nodeId IS NULL OR ({Constants.DatabaseSchema.Tables.Document}.published=1 AND nuPublished.nodeId IS NULL);" - , new { objType = contentObjectType }); - - return count == 0; - } - public bool VerifyMediaDbCache() { + // TODO: Shouldn't this entire logic just exist in the call to _publishedContentService? using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.MediaTree); - var ok = VerifyMediaDbCacheLocked(scope); + var ok = _publishedContentService.VerifyMediaDbCache(); scope.Complete(); return ok; } } - // assumes media tree lock - public bool VerifyMediaDbCacheLocked(IScope scope) - { - // every media item should have a corresponding row for edited properties - - var mediaObjectType = Constants.ObjectTypes.Media; - var db = scope.Database; - - var count = db.ExecuteScalar(@"SELECT COUNT(*) -FROM umbracoNode -LEFT JOIN cmsContentNu ON (umbracoNode.id=cmsContentNu.nodeId AND cmsContentNu.published=0) -WHERE umbracoNode.nodeObjectType=@objType -AND cmsContentNu.nodeId IS NULL -", new { objType = mediaObjectType }); - - return count == 0; - } - public bool VerifyMemberDbCache() { + // TODO: Shouldn't this entire logic just exist in the call to _publishedContentService? using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.MemberTree); - var ok = VerifyMemberDbCacheLocked(scope); + var ok = _publishedContentService.VerifyMemberDbCache(); scope.Complete(); return ok; } } - // assumes member tree lock - public bool VerifyMemberDbCacheLocked(IScope scope) - { - // every member item should have a corresponding row for edited properties - - var memberObjectType = Constants.ObjectTypes.Member; - var db = scope.Database; - - var count = db.ExecuteScalar(@"SELECT COUNT(*) -FROM umbracoNode -LEFT JOIN cmsContentNu ON (umbracoNode.id=cmsContentNu.nodeId AND cmsContentNu.published=0) -WHERE umbracoNode.nodeObjectType=@objType -AND cmsContentNu.nodeId IS NULL -", new { objType = memberObjectType }); - - return count == 0; - } - #endregion #region Instrument diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs new file mode 100644 index 0000000000..20ce74ea70 --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs @@ -0,0 +1,187 @@ +using System; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Changes; +using Umbraco.Core.Services.Implement; +using Umbraco.Infrastructure.PublishedCache.Persistence; + +namespace Umbraco.Web.PublishedCache.NuCache +{ + public class PublishedSnapshotServiceEventHandler : IDisposable + { + private readonly IRuntimeState _runtime; + private bool _disposedValue; + private readonly IPublishedSnapshotService _publishedSnapshotService; + private readonly INuCacheContentService _publishedContentService; + + public PublishedSnapshotServiceEventHandler( + IRuntimeState runtime, + IPublishedSnapshotService publishedSnapshotService, + INuCacheContentService publishedContentService) + { + _runtime = runtime; + _publishedSnapshotService = publishedSnapshotService; + _publishedContentService = publishedContentService; + } + + public bool Start() + { + // however, the cache is NOT available until we are configured, because loading + // content (and content types) from database cannot be consistent (see notes in "Handle + // Notifications" region), so + // - notifications will be ignored + // - trying to obtain a published snapshot from the service will throw + if (_runtime.Level != RuntimeLevel.Run) + { + return false; + } + + // this initializes the caches. + // TODO: This is still temporal coupling (i.e. Initialize) + _publishedSnapshotService.LoadCachesOnStartup(); + + // we always want to handle repository events, configured or not + // assuming no repository event will trigger before the whole db is ready + // (ideally we'd have Upgrading.App vs Upgrading.Data application states...) + InitializeRepositoryEvents(); + + return true; + } + + private void InitializeRepositoryEvents() + { + // TODO: The reason these events are in the repository is for legacy, the events should exist at the service + // level now since we can fire these events within the transaction... so move the events to service level + + // plug repository event handlers + // these trigger within the transaction to ensure consistency + // and are used to maintain the central, database-level XML cache + DocumentRepository.ScopeEntityRemove += OnContentRemovingEntity; + DocumentRepository.ScopedEntityRefresh += DocumentRepository_ScopedEntityRefresh; + MediaRepository.ScopeEntityRemove += OnMediaRemovingEntity; + MediaRepository.ScopedEntityRefresh += MediaRepository_ScopedEntityRefresh; + MemberRepository.ScopeEntityRemove += OnMemberRemovingEntity; + MemberRepository.ScopedEntityRefresh += MemberRepository_ScopedEntityRefresh; + + // plug + ContentTypeService.ScopedRefreshedEntity += OnContentTypeRefreshedEntity; + MediaTypeService.ScopedRefreshedEntity += OnMediaTypeRefreshedEntity; + MemberTypeService.ScopedRefreshedEntity += OnMemberTypeRefreshedEntity; + + // TODO: This should be a cache refresher call! + LocalizationService.SavedLanguage += OnLanguageSaved; + } + + private void TearDownRepositoryEvents() + { + DocumentRepository.ScopeEntityRemove -= OnContentRemovingEntity; + DocumentRepository.ScopedEntityRefresh -= DocumentRepository_ScopedEntityRefresh; + MediaRepository.ScopeEntityRemove -= OnMediaRemovingEntity; + MediaRepository.ScopedEntityRefresh -= MediaRepository_ScopedEntityRefresh; + MemberRepository.ScopeEntityRemove -= OnMemberRemovingEntity; + MemberRepository.ScopedEntityRefresh -= MemberRepository_ScopedEntityRefresh; + ContentTypeService.ScopedRefreshedEntity -= OnContentTypeRefreshedEntity; + MediaTypeService.ScopedRefreshedEntity -= OnMediaTypeRefreshedEntity; + MemberTypeService.ScopedRefreshedEntity -= OnMemberTypeRefreshedEntity; + LocalizationService.SavedLanguage -= OnLanguageSaved; // TODO: Shouldn't this be a cache refresher event? + } + + // note: if the service is not ready, ie _isReady is false, then we still handle repository events, + // because we can, we do not need a working published snapshot to do it - the only reason why it could cause an + // issue is if the database table is not ready, but that should be prevented by migrations. + + // we need them to be "repository" events ie to trigger from within the repository transaction, + // because they need to be consistent with the content that is being refreshed/removed - and that + // should be guaranteed by a DB transaction + private void OnContentRemovingEntity(DocumentRepository sender, DocumentRepository.ScopedEntityEventArgs args) + => _publishedContentService.DeleteContentItem(args.Entity); + + private void OnMediaRemovingEntity(MediaRepository sender, MediaRepository.ScopedEntityEventArgs args) + => _publishedContentService.DeleteContentItem(args.Entity); + + private void OnMemberRemovingEntity(MemberRepository sender, MemberRepository.ScopedEntityEventArgs args) + => _publishedContentService.DeleteContentItem(args.Entity); + + private void MemberRepository_ScopedEntityRefresh(MemberRepository sender, ContentRepositoryBase.ScopedEntityEventArgs e) + => _publishedContentService.RefreshEntity(e.Entity); + + private void MediaRepository_ScopedEntityRefresh(MediaRepository sender, ContentRepositoryBase.ScopedEntityEventArgs e) + => _publishedContentService.RefreshEntity(e.Entity); + + private void DocumentRepository_ScopedEntityRefresh(DocumentRepository sender, ContentRepositoryBase.ScopedEntityEventArgs e) + => _publishedContentService.RefreshContent(e.Entity); + + private void OnContentTypeRefreshedEntity(IContentTypeService sender, ContentTypeChange.EventArgs args) + { + const ContentTypeChangeTypes types // only for those that have been refreshed + = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther; + var contentTypeIds = args.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id).ToArray(); + if (contentTypeIds.Any()) + { + _publishedSnapshotService.Rebuild(contentTypeIds: contentTypeIds); + } + } + + private void OnMediaTypeRefreshedEntity(IMediaTypeService sender, ContentTypeChange.EventArgs args) + { + const ContentTypeChangeTypes types // only for those that have been refreshed + = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther; + var mediaTypeIds = args.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id).ToArray(); + if (mediaTypeIds.Any()) + { + _publishedSnapshotService.Rebuild(mediaTypeIds: mediaTypeIds); + } + } + + private void OnMemberTypeRefreshedEntity(IMemberTypeService sender, ContentTypeChange.EventArgs args) + { + const ContentTypeChangeTypes types // only for those that have been refreshed + = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther; + var memberTypeIds = args.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id).ToArray(); + if (memberTypeIds.Any()) + { + _publishedSnapshotService.Rebuild(memberTypeIds: memberTypeIds); + } + } + + /// + /// If a is ever saved with a different culture, we need to rebuild all of the content nucache table + /// + private void OnLanguageSaved(ILocalizationService sender, Core.Events.SaveEventArgs e) + { + // TODO: This should be a cache refresher call! + + // culture changed on an existing language + var cultureChanged = e.SavedEntities.Any(x => !x.WasPropertyDirty(nameof(ILanguage.Id)) && x.WasPropertyDirty(nameof(ILanguage.IsoCode))); + if (cultureChanged) + { + // Rebuild all content types + _publishedSnapshotService.Rebuild(contentTypeIds: Array.Empty()); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + TearDownRepositoryEvents(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index b0ba97eedf..e0ef0434c9 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -114,7 +114,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Persistence.Repositor var languageRepository = new LanguageRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), globalSettings); contentTypeRepository = new ContentTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), commonRepository, languageRepository, ShortStringHelper); var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger()); - var entityRepository = new EntityRepository(scopeAccessor); + var entityRepository = new EntityRepository(scopeAccessor, AppCaches.Disabled); var relationRepository = new RelationRepository(scopeAccessor, LoggerFactory.CreateLogger(), relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/EntityRepositoryTest.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/EntityRepositoryTest.cs index 3fb518661c..8fbfc765d7 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/EntityRepositoryTest.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/EntityRepositoryTest.cs @@ -1,7 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Repositories.Implement; @@ -19,7 +20,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Persistence.Repositor { private EntityRepository CreateRepository(IScopeAccessor scopeAccessor) { - var entityRepository = new EntityRepository(scopeAccessor); + var entityRepository = new EntityRepository(scopeAccessor, AppCaches.Disabled); return entityRepository; } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs index 7d3dcc7202..f504218cec 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using Microsoft.Extensions.Logging; using Moq; @@ -53,7 +53,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Persistence.Repositor mediaTypeRepository = new MediaTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), commonRepository, languageRepository, ShortStringHelper); var tagRepository = new TagRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger()); var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger()); - var entityRepository = new EntityRepository(scopeAccessor); + var entityRepository = new EntityRepository(scopeAccessor, AppCaches.Disabled); var relationRepository = new RelationRepository(scopeAccessor, LoggerFactory.CreateLogger(), relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); var mediaUrlGenerators = new MediaUrlGeneratorCollection(Enumerable.Empty()); diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs index 26efdf325a..b73af3fdfc 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs @@ -19,7 +19,7 @@ using Umbraco.Tests.Testing; namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Persistence.Repositories { [TestFixture] - [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Boot = true)] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] public class RelationRepositoryTest : UmbracoIntegrationTest { private RelationType _relateContent; @@ -432,7 +432,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Persistence.Repositor { var accessor = (IScopeAccessor)ScopeProvider; var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Mock.Of>()); - var entityRepository = new EntityRepository(accessor); + var entityRepository = new EntityRepository(accessor, AppCaches.Disabled); var relationRepository = new RelationRepository(accessor, Mock.Of>(), relationTypeRepository, entityRepository); relationTypeRepository.Save(_relateContent); diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs index 6deb579ad1..5cab23c4c9 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -262,7 +262,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Persistence.Repositor var languageRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger(), Microsoft.Extensions.Options.Options.Create(globalSettings)); var contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger(), commonRepository, languageRepository, ShortStringHelper); var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger()); - var entityRepository = new EntityRepository(scopeAccessor); + var entityRepository = new EntityRepository(scopeAccessor, AppCaches.Disabled); var relationRepository = new RelationRepository(scopeAccessor, LoggerFactory.CreateLogger(), relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs index b7ab3ceea1..4f4a852902 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -42,10 +42,9 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services [SetUp] public void SetupTestData() { - - //This is super nasty, but this lets us initialize the cache while it is empty. - var publishedSnapshotService = GetRequiredService() as PublishedSnapshotService; - publishedSnapshotService?.OnApplicationInit(null, EventArgs.Empty); + // This is super nasty, but this lets us initialize the cache while it is empty. + // var publishedSnapshotService = GetRequiredService() as PublishedSnapshotService; + // publishedSnapshotService?.OnApplicationInit(null, EventArgs.Empty); if (_langFr == null && _langEs == null) { diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index a17eb71ab0..8e5250499c 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -33,6 +33,7 @@ using Umbraco.Web.PublishedCache.NuCache.DataSource; using Current = Umbraco.Web.Composing.Current; using Umbraco.Core.Serialization; using Umbraco.Net; +using Umbraco.Infrastructure.PublishedCache.Persistence; namespace Umbraco.Tests.PublishedContent { @@ -148,11 +149,9 @@ namespace Umbraco.Tests.PublishedContent // at last, create the complete NuCache snapshot service! var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }; - var lifetime = new Mock(); - _snapshotService = new PublishedSnapshotService(options, + _snapshotService = new PublishedSnapshotService( + options, null, - lifetime.Object, - runtime, serviceContext, contentTypeFactory, _snapshotAccessor, @@ -160,23 +159,17 @@ namespace Umbraco.Tests.PublishedContent Mock.Of(), NullLoggerFactory.Instance, scopeProvider.Object, - Mock.Of(), - Mock.Of(), - Mock.Of(), - new TestDefaultCultureAccessor(), _source, + new TestDefaultCultureAccessor(), Options.Create(globalSettings), Mock.Of(), PublishedModelFactory, - new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider(TestHelper.ShortStringHelper) }), hostingEnvironment, - Mock.Of(), TestHelper.IOHelper, Options.Create(nuCacheSettings)); // invariant is the current default _variationAccesor.VariationContext = new VariationContext(); - lifetime.Raise(e => e.ApplicationInit += null, EventArgs.Empty); Mock.Get(factory).Setup(x => x.GetService(typeof(IVariationContextAccessor))).Returns(_variationAccesor); } diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs index d689215081..bc87967780 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -189,11 +189,9 @@ namespace Umbraco.Tests.PublishedContent // at last, create the complete NuCache snapshot service! var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }; - var lifetime = new Mock(); - _snapshotService = new PublishedSnapshotService(options, + _snapshotService = new PublishedSnapshotService( + options, null, - lifetime.Object, - runtime, serviceContext, contentTypeFactory, new TestPublishedSnapshotAccessor(), @@ -201,22 +199,15 @@ namespace Umbraco.Tests.PublishedContent Mock.Of(), NullLoggerFactory.Instance, scopeProvider, - Mock.Of(), - Mock.Of(), - Mock.Of(), - new TestDefaultCultureAccessor(), dataSource, + new TestDefaultCultureAccessor(), Microsoft.Extensions.Options.Options.Create(globalSettings), Mock.Of(), publishedModelFactory, - new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider(TestHelper.ShortStringHelper) }), TestHelper.GetHostingEnvironment(), - Mock.Of(), TestHelper.IOHelper, Microsoft.Extensions.Options.Options.Create(nuCacheSettings)); - lifetime.Raise(e => e.ApplicationInit += null, EventArgs.Empty); - // invariant is the current default _variationAccesor.VariationContext = new VariationContext(); diff --git a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs index 1a8e485634..cd733abad2 100644 --- a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Web.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -17,6 +17,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; using Umbraco.Core.Sync; +using Umbraco.Infrastructure.PublishedCache.Persistence; using Umbraco.Net; using Umbraco.Tests.Common; using Umbraco.Tests.TestHelpers; @@ -71,7 +72,7 @@ namespace Umbraco.Tests.Scoping protected override IPublishedSnapshotService CreatePublishedSnapshotService(GlobalSettings globalSettings = null) { var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }; - var publishedSnapshotAccessor = new UmbracoContextPublishedSnapshotAccessor(Umbraco.Web.Composing.Current.UmbracoContextAccessor); + var publishedSnapshotAccessor = new UmbracoContextPublishedSnapshotAccessor(Current.UmbracoContextAccessor); var runtimeStateMock = new Mock(); runtimeStateMock.Setup(x => x.Level).Returns(() => RuntimeLevel.Run); @@ -85,27 +86,23 @@ namespace Umbraco.Tests.Scoping var nuCacheSettings = new NuCacheSettings(); var lifetime = new Mock(); + var repository = new NuCacheContentRepository(ScopeProvider, AppCaches.Disabled, Mock.Of>(), memberRepository, documentRepository, mediaRepository, Mock.Of(), new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider(ShortStringHelper) })); var snapshotService = new PublishedSnapshotService( options, null, - lifetime.Object, - runtimeStateMock.Object, ServiceContext, contentTypeFactory, publishedSnapshotAccessor, Mock.Of(), - ProfilingLogger, + base.ProfilingLogger, NullLoggerFactory.Instance, ScopeProvider, - documentRepository, mediaRepository, memberRepository, + new NuCacheContentService(repository, ScopeProvider, NullLoggerFactory.Instance, Mock.Of()), DefaultCultureAccessor, - new DatabaseDataSource(Mock.Of>()), Microsoft.Extensions.Options.Options.Create(globalSettings ?? new GlobalSettings()), Factory.GetRequiredService(), new NoopPublishedModelFactory(), - new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider(ShortStringHelper) }), hostingEnvironment, - Mock.Of(), IOHelper, Microsoft.Extensions.Options.Options.Create(nuCacheSettings)); diff --git a/src/Umbraco.Tests/Testing/Objects/TestDataSource.cs b/src/Umbraco.Tests/Testing/Objects/TestDataSource.cs index fc8a48581a..d8a413fab9 100644 --- a/src/Umbraco.Tests/Testing/Objects/TestDataSource.cs +++ b/src/Umbraco.Tests/Testing/Objects/TestDataSource.cs @@ -1,16 +1,17 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Scoping; +using Umbraco.Infrastructure.PublishedCache.Persistence; using Umbraco.Web; using Umbraco.Web.PublishedCache.NuCache; -using Umbraco.Web.PublishedCache.NuCache.DataSource; namespace Umbraco.Tests.Testing.Objects { - internal class TestDataSource : IDataSource + internal class TestDataSource : INuCacheContentService { private IPublishedModelFactory PublishedModelFactory { get; } = new NoopPublishedModelFactory(); @@ -19,27 +20,23 @@ namespace Umbraco.Tests.Testing.Objects : this((IEnumerable) kits) { } - public TestDataSource(IEnumerable kits) - { - Kits = kits.ToDictionary(x => x.Node.Id, x => x); - } + public TestDataSource(IEnumerable kits) => Kits = kits.ToDictionary(x => x.Node.Id, x => x); public Dictionary Kits { get; } // note: it is important to clone the returned kits, as the inner // ContentNode is directly reused and modified by the snapshot service + public ContentNodeKit GetContentSource(int id) + => Kits.TryGetValue(id, out ContentNodeKit kit) ? kit.Clone(PublishedModelFactory) : default; - public ContentNodeKit GetContentSource(IScope scope, int id) - => Kits.TryGetValue(id, out var kit) ? kit.Clone(PublishedModelFactory) : default; - - public IEnumerable GetAllContentSources(IScope scope) + public IEnumerable GetAllContentSources() => Kits.Values .OrderBy(x => x.Node.Level) .ThenBy(x => x.Node.ParentContentId) .ThenBy(x => x.Node.SortOrder) .Select(x => x.Clone(PublishedModelFactory)); - public IEnumerable GetBranchContentSources(IScope scope, int id) + public IEnumerable GetBranchContentSources(int id) => Kits.Values .Where(x => x.Node.Path.EndsWith("," + id) || x.Node.Path.Contains("," + id + ",")) .OrderBy(x => x.Node.Level) @@ -47,7 +44,7 @@ namespace Umbraco.Tests.Testing.Objects .ThenBy(x => x.Node.SortOrder) .Select(x => x.Clone(PublishedModelFactory)); - public IEnumerable GetTypeContentSources(IScope scope, IEnumerable ids) + public IEnumerable GetTypeContentSources(IEnumerable ids) => Kits.Values .Where(x => ids.Contains(x.ContentTypeId)) .OrderBy(x => x.Node.Level) @@ -55,24 +52,19 @@ namespace Umbraco.Tests.Testing.Objects .ThenBy(x => x.Node.SortOrder) .Select(x => x.Clone(PublishedModelFactory)); - public ContentNodeKit GetMediaSource(IScope scope, int id) - { - return default; - } + public ContentNodeKit GetMediaSource(int id) => default; - public IEnumerable GetAllMediaSources(IScope scope) - { - return Enumerable.Empty(); - } + public IEnumerable GetAllMediaSources() => Enumerable.Empty(); - public IEnumerable GetBranchMediaSources(IScope scope, int id) - { - return Enumerable.Empty(); - } + public IEnumerable GetBranchMediaSources(int id) => Enumerable.Empty(); - public IEnumerable GetTypeMediaSources(IScope scope, IEnumerable ids) - { - return Enumerable.Empty(); - } + public IEnumerable GetTypeMediaSources(IEnumerable ids) => Enumerable.Empty(); + public void DeleteContentItem(IContentBase item) => throw new NotImplementedException(); + public void RefreshContent(IContent content) => throw new NotImplementedException(); + public void RefreshEntity(IContentBase content) => throw new NotImplementedException(); + public bool VerifyContentDbCache() => throw new NotImplementedException(); + public bool VerifyMediaDbCache() => throw new NotImplementedException(); + public bool VerifyMemberDbCache() => throw new NotImplementedException(); + public void Rebuild(int groupSize = 5000, IReadOnlyCollection contentTypeIds = null, IReadOnlyCollection mediaTypeIds = null, IReadOnlyCollection memberTypeIds = null) => throw new NotImplementedException(); } } diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 99a2b2aa3f..7290aa9b0e 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -11,6 +11,7 @@ using Umbraco.Core; using Umbraco.Core.Hosting; using Umbraco.Infrastructure.Logging.Serilog.Enrichers; using Umbraco.Web.Common.Middleware; +using Umbraco.Web.PublishedCache.NuCache; namespace Umbraco.Extensions { @@ -36,6 +37,7 @@ namespace Umbraco.Extensions // We need to add this before UseRouting so that the UmbracoContext and other middlewares are executed // before endpoint routing middleware. app.UseUmbracoRouting(); + app.UseUmbracoContentCache(); app.UseStatusCodePages(); @@ -176,6 +178,16 @@ namespace Umbraco.Extensions return app; } + /// + /// Enables the Umbraco content cache + /// + public static IApplicationBuilder UseUmbracoContentCache(this IApplicationBuilder app) + { + PublishedSnapshotServiceEventHandler publishedContentEvents = app.ApplicationServices.GetRequiredService(); + publishedContentEvents.Start(); + return app; + } + /// /// Ensures the runtime is shutdown when the application is shutting down /// diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs index 6b5d305a64..56f093ed2b 100644 --- a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs @@ -55,7 +55,7 @@ namespace Umbraco.Web.Common.Middleware return; } - _backofficeSecurityFactory.EnsureBackOfficeSecurity(); // Needs to be before UmbracoContext + _backofficeSecurityFactory.EnsureBackOfficeSecurity(); // Needs to be before UmbracoContext, TODO: Why? UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); try diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index b09fde0a6a..fdc488c65b 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index a6582f03ae..328b7cc1d4 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -190,7 +190,7 @@ namespace Umbraco.Web.Website.Routing { ControllerActionDescriptor descriptor = _actionDescriptorCollectionProvider.ActionDescriptors.Items .Cast() - .First(x => + .FirstOrDefault(x => x.ControllerName.Equals(controllerName)); return descriptor?.ControllerTypeInfo; From 63ab8ec52c773c0d5c22b67b5c30500df0b2f0c0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Dec 2020 18:09:32 +1100 Subject: [PATCH 03/14] Lots of notes, removes data tokens, --- src/Umbraco.Core/Constants-Web.cs | 11 +- .../IPublishedSnapshotService.cs | 7 + src/Umbraco.Core/Routing/IPublishedRequest.cs | 2 +- src/Umbraco.Infrastructure/Scoping/Scope.cs | 32 ++-- .../Scoping/ScopeProvider.cs | 12 +- .../Umbraco.Core/Components/ComponentTests.cs | 3 +- .../ModelBinders/ContentModelBinderTests.cs | 27 ++- .../ModelBinders/RenderModelBinderTests.cs | 23 ++- ...RenderIndexActionSelectorAttributeTests.cs | 169 ------------------ .../Controllers/SurfaceControllerTests.cs | 11 +- .../XmlPublishedSnapshotService.cs | 6 +- .../Scoping/ScopeEventDispatcherTests.cs | 20 +-- .../TestHelpers/BaseUsingSqlCeSyntax.cs | 4 +- src/Umbraco.Tests/TestHelpers/TestObjects.cs | 31 +--- src/Umbraco.Tests/Testing/UmbracoTestBase.cs | 9 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 - .../Web/Mvc/SurfaceControllerTests.cs | 4 +- .../Web/WebExtensionMethodTests.cs | 139 -------------- .../AspNetCore/UmbracoViewPage.cs | 7 +- .../Controllers/RenderController.cs | 37 ++-- .../Macros/MacroRenderer.cs | 35 ++-- .../Macros/PartialViewMacroEngine.cs | 119 ++++++------ .../ModelBinders/ContentModelBinder.cs | 33 ++-- .../Routing/UmbracoRouteValues.cs | 68 +++++++ .../ActionResults/UmbracoPageResult.cs | 4 +- .../RenderIndexActionSelectorAttribute.cs | 60 ------- .../Controllers/SurfaceController.cs | 32 ++-- .../Controllers/UmbracoRenderingDefaults.cs | 1 + .../Routing/RouteDefinition.cs | 29 --- .../Routing/UmbracoRouteValueTransformer.cs | 123 ++++++------- .../ViewEngines/PluginViewEngine.cs | 26 +-- .../ViewEngines/RenderViewEngine.cs | 59 +++--- .../Mvc/AreaRegistrationExtensions.cs | 6 +- .../Mvc/ControllerContextExtensions.cs | 38 ---- src/Umbraco.Web/Mvc/RenderRouteHandler.cs | 25 +-- src/Umbraco.Web/Mvc/SurfaceController.cs | 8 +- src/Umbraco.Web/Mvc/UmbracoPageResult.cs | 6 +- .../Mvc/UmbracoViewPageOfTModel.cs | 42 ++--- .../Mvc/UmbracoVirtualNodeRouteHandler.cs | 14 +- .../Runtime/WebInitialComponent.cs | 11 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 - 41 files changed, 464 insertions(+), 831 deletions(-) delete mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttributeTests.cs delete mode 100644 src/Umbraco.Tests/Web/WebExtensionMethodTests.cs rename src/{Umbraco.Web.Website => Umbraco.Web.Common}/Controllers/RenderController.cs (63%) create mode 100644 src/Umbraco.Web.Common/Routing/UmbracoRouteValues.cs delete mode 100644 src/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttribute.cs delete mode 100644 src/Umbraco.Web.Website/Routing/RouteDefinition.cs delete mode 100644 src/Umbraco.Web/Mvc/ControllerContextExtensions.cs diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index 5d059d8a23..e29d793909 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Core +namespace Umbraco.Core { public static partial class Constants { @@ -7,10 +7,11 @@ /// public static class Web { - public const string UmbracoContextDataToken = "umbraco-context"; - public const string UmbracoDataToken = "umbraco"; - public const string PublishedDocumentRequestDataToken = "umbraco-doc-request"; - public const string CustomRouteDataToken = "umbraco-custom-route"; + // TODO: Need to review these... + //public const string UmbracoContextDataToken = "umbraco-context"; + //public const string UmbracoDataToken = "umbraco"; + //public const string PublishedDocumentRequestDataToken = "umbraco-doc-request"; + //public const string CustomRouteDataToken = "umbraco-custom-route"; public const string UmbracoRouteDefinitionDataToken = "umbraco-route-def"; /// diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs index 73ce858b52..cc526ffe6e 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs @@ -23,6 +23,13 @@ namespace Umbraco.Web.PublishedCache * */ + /// + /// Loads the caches on startup - called once during startup + /// TODO: Temporary, this is temporal coupling, we cannot use IUmbracoApplicationLifetime.ApplicationInit (which we want to delete) + /// handler because that is executed with netcore's IHostApplicationLifetime.ApplicationStarted mechanism which fires async + /// which we don't want since this will not have initialized before our endpoints execute. So for now this is explicitly + /// called on UseUmbracoContentCaching on startup. + /// void LoadCachesOnStartup(); /// diff --git a/src/Umbraco.Core/Routing/IPublishedRequest.cs b/src/Umbraco.Core/Routing/IPublishedRequest.cs index f357108a4e..51fc9ccf64 100644 --- a/src/Umbraco.Core/Routing/IPublishedRequest.cs +++ b/src/Umbraco.Core/Routing/IPublishedRequest.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.Routing /// /// Gets the UmbracoContext. /// - IUmbracoContext UmbracoContext { get; } + IUmbracoContext UmbracoContext { get; } // TODO: This should be injected and removed from here /// /// Gets or sets the cleaned up Uri used for routing. diff --git a/src/Umbraco.Infrastructure/Scoping/Scope.cs b/src/Umbraco.Infrastructure/Scoping/Scope.cs index 65e8e343f7..84945c78d4 100644 --- a/src/Umbraco.Infrastructure/Scoping/Scope.cs +++ b/src/Umbraco.Infrastructure/Scoping/Scope.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Data; using Microsoft.Extensions.Logging; using Umbraco.Core.Cache; @@ -20,7 +20,6 @@ namespace Umbraco.Core.Scoping private readonly CoreDebugSettings _coreDebugSettings; private readonly IMediaFileSystem _mediaFileSystem; private readonly ILogger _logger; - private readonly ITypeFinder _typeFinder; private readonly IsolationLevel _isolationLevel; private readonly RepositoryCacheMode _repositoryCacheMode; @@ -38,10 +37,15 @@ namespace Umbraco.Core.Scoping private IEventDispatcher _eventDispatcher; // initializes a new scope - private Scope(ScopeProvider scopeProvider, + private Scope( + ScopeProvider scopeProvider, CoreDebugSettings coreDebugSettings, IMediaFileSystem mediaFileSystem, - ILogger logger, ITypeFinder typeFinder, FileSystems fileSystems, Scope parent, IScopeContext scopeContext, bool detachable, + ILogger logger, + FileSystems fileSystems, + Scope parent, + IScopeContext scopeContext, + bool detachable, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, IEventDispatcher eventDispatcher = null, @@ -53,7 +57,6 @@ namespace Umbraco.Core.Scoping _coreDebugSettings = coreDebugSettings; _mediaFileSystem = mediaFileSystem; _logger = logger; - _typeFinder = typeFinder; Context = scopeContext; @@ -117,31 +120,38 @@ namespace Umbraco.Core.Scoping } // initializes a new scope - public Scope(ScopeProvider scopeProvider, + public Scope( + ScopeProvider scopeProvider, CoreDebugSettings coreDebugSettings, IMediaFileSystem mediaFileSystem, - ILogger logger, ITypeFinder typeFinder, FileSystems fileSystems, bool detachable, IScopeContext scopeContext, + ILogger logger, + FileSystems fileSystems, + bool detachable, + IScopeContext scopeContext, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, IEventDispatcher eventDispatcher = null, bool? scopeFileSystems = null, bool callContext = false, bool autoComplete = false) - : this(scopeProvider, coreDebugSettings, mediaFileSystem, logger, typeFinder, fileSystems, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete) + : this(scopeProvider, coreDebugSettings, mediaFileSystem, logger, fileSystems, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete) { } // initializes a new scope in a nested scopes chain, with its parent - public Scope(ScopeProvider scopeProvider, + public Scope( + ScopeProvider scopeProvider, CoreDebugSettings coreDebugSettings, IMediaFileSystem mediaFileSystem, - ILogger logger, ITypeFinder typeFinder, FileSystems fileSystems, Scope parent, + ILogger logger, + FileSystems fileSystems, + Scope parent, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, IEventDispatcher eventDispatcher = null, bool? scopeFileSystems = null, bool callContext = false, bool autoComplete = false) - : this(scopeProvider, coreDebugSettings, mediaFileSystem, logger, typeFinder, fileSystems, parent, null, false, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete) + : this(scopeProvider, coreDebugSettings, mediaFileSystem, logger, fileSystems, parent, null, false, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete) { } public Guid InstanceId { get; } = Guid.NewGuid(); diff --git a/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs b/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs index 52c096b224..151c4cfb3c 100644 --- a/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs +++ b/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Data; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -23,13 +23,12 @@ namespace Umbraco.Core.Scoping { private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; - private readonly ITypeFinder _typeFinder; private readonly IRequestCache _requestCache; private readonly FileSystems _fileSystems; private readonly CoreDebugSettings _coreDebugSettings; private readonly IMediaFileSystem _mediaFileSystem; - public ScopeProvider(IUmbracoDatabaseFactory databaseFactory, FileSystems fileSystems, IOptions coreDebugSettings, IMediaFileSystem mediaFileSystem, ILogger logger, ILoggerFactory loggerFactory, ITypeFinder typeFinder, IRequestCache requestCache) + public ScopeProvider(IUmbracoDatabaseFactory databaseFactory, FileSystems fileSystems, IOptions coreDebugSettings, IMediaFileSystem mediaFileSystem, ILogger logger, ILoggerFactory loggerFactory, IRequestCache requestCache) { DatabaseFactory = databaseFactory; _fileSystems = fileSystems; @@ -37,7 +36,6 @@ namespace Umbraco.Core.Scoping _mediaFileSystem = mediaFileSystem; _logger = logger; _loggerFactory = loggerFactory; - _typeFinder = typeFinder; _requestCache = requestCache; // take control of the FileSystems _fileSystems.IsScoped = () => AmbientScope != null && AmbientScope.ScopedFileSystems; @@ -256,7 +254,7 @@ namespace Umbraco.Core.Scoping IEventDispatcher eventDispatcher = null, bool? scopeFileSystems = null) { - return new Scope(this, _coreDebugSettings, _mediaFileSystem, _loggerFactory.CreateLogger(), _typeFinder, _fileSystems, true, null, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems); + return new Scope(this, _coreDebugSettings, _mediaFileSystem, _loggerFactory.CreateLogger(), _fileSystems, true, null, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems); } /// @@ -312,13 +310,13 @@ namespace Umbraco.Core.Scoping { var ambientContext = AmbientContext; var newContext = ambientContext == null ? new ScopeContext() : null; - var scope = new Scope(this, _coreDebugSettings, _mediaFileSystem, _loggerFactory.CreateLogger(), _typeFinder, _fileSystems, false, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete); + var scope = new Scope(this, _coreDebugSettings, _mediaFileSystem, _loggerFactory.CreateLogger(), _fileSystems, false, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete); // assign only if scope creation did not throw! SetAmbient(scope, newContext ?? ambientContext); return scope; } - var nested = new Scope(this, _coreDebugSettings, _mediaFileSystem, _loggerFactory.CreateLogger(), _typeFinder, _fileSystems, ambientScope, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete); + var nested = new Scope(this, _coreDebugSettings, _mediaFileSystem, _loggerFactory.CreateLogger(), _fileSystems, ambientScope, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete); SetAmbient(nested, AmbientContext); return nested; } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs index 44aacab944..3bc29c9a9d 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs @@ -39,14 +39,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Components var mock = new Mock(); var loggerFactory = NullLoggerFactory.Instance; var logger = loggerFactory.CreateLogger("GenericLogger"); - var typeFinder = TestHelper.GetTypeFinder(); var globalSettings = new GlobalSettings(); var connectionStrings = new ConnectionStrings(); var f = new UmbracoDatabaseFactory(loggerFactory.CreateLogger(), loggerFactory, Options.Create(globalSettings), Options.Create(connectionStrings), new Lazy(() => new MapperCollection(Enumerable.Empty())), TestHelper.DbProviderFactoryCreator); var fs = new FileSystems(mock.Object, loggerFactory.CreateLogger(), loggerFactory, IOHelper, Options.Create(globalSettings), Mock.Of()); var coreDebug = new CoreDebugSettings(); var mediaFileSystem = Mock.Of(); - var p = new ScopeProvider(f, fs, Options.Create(coreDebug), mediaFileSystem, loggerFactory.CreateLogger(), loggerFactory, typeFinder, NoAppCache.Instance); + var p = new ScopeProvider(f, fs, Options.Create(coreDebug), mediaFileSystem, loggerFactory.CreateLogger(), loggerFactory, NoAppCache.Instance); mock.Setup(x => x.GetService(typeof(ILogger))).Returns(logger); mock.Setup(x => x.GetService(typeof(ILogger))).Returns(loggerFactory.CreateLogger); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs index b414e49e95..ba5910da29 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -9,7 +9,9 @@ using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Common.ModelBinders; +using Umbraco.Web.Common.Routing; using Umbraco.Web.Models; +using Umbraco.Web.Website.Routing; namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders { @@ -20,7 +22,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders public void Does_Not_Bind_Model_When_UmbracoDataToken_Not_In_Route_Data() { // Arrange - var bindingContext = CreateBindingContext(typeof(ContentModel), withUmbracoDataToken: false); + IPublishedContent pc = CreatePublishedContent(); + var bindingContext = CreateBindingContext(typeof(ContentModel), pc, withUmbracoDataToken: false); var binder = new ContentModelBinder(); // Act @@ -34,7 +37,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders public void Does_Not_Bind_Model_When_Source_Not_Of_Expected_Type() { // Arrange - var bindingContext = CreateBindingContext(typeof(ContentModel), source: new NonContentModel()); + IPublishedContent pc = CreatePublishedContent(); + var bindingContext = CreateBindingContext(typeof(ContentModel), pc, source: new NonContentModel()); var binder = new ContentModelBinder(); // Act @@ -48,8 +52,9 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders public void BindModel_Returns_If_Same_Type() { // Arrange - var content = new ContentModel(CreatePublishedContent()); - var bindingContext = CreateBindingContext(typeof(ContentModel), source: content); + IPublishedContent pc = CreatePublishedContent(); + var content = new ContentModel(pc); + var bindingContext = CreateBindingContext(typeof(ContentModel), pc, source: content); var binder = new ContentModelBinder(); // Act @@ -63,7 +68,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders public void Binds_From_IPublishedContent_To_Content_Model() { // Arrange - var bindingContext = CreateBindingContext(typeof(ContentModel), source: CreatePublishedContent()); + IPublishedContent pc = CreatePublishedContent(); + var bindingContext = CreateBindingContext(typeof(ContentModel), pc, source: pc); var binder = new ContentModelBinder(); // Act @@ -77,7 +83,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders public void Binds_From_IPublishedContent_To_Content_Model_Of_T() { // Arrange - var bindingContext = CreateBindingContext(typeof(ContentModel), source: new ContentModel(new ContentType2(CreatePublishedContent()))); + IPublishedContent pc = CreatePublishedContent(); + var bindingContext = CreateBindingContext(typeof(ContentModel), pc, source: new ContentModel(new ContentType2(pc))); var binder = new ContentModelBinder(); // Act @@ -87,12 +94,14 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders Assert.True(bindingContext.Result.IsModelSet); } - private ModelBindingContext CreateBindingContext(Type modelType, bool withUmbracoDataToken = true, object source = null) + private ModelBindingContext CreateBindingContext(Type modelType, IPublishedContent publishedContent, bool withUmbracoDataToken = true, object source = null) { var httpContext = new DefaultHttpContext(); var routeData = new RouteData(); if (withUmbracoDataToken) - routeData.DataTokens.Add(Constants.Web.UmbracoDataToken, source); + { + routeData.Values.Add(Constants.Web.UmbracoRouteDefinitionDataToken, new UmbracoRouteValues(publishedContent)); + } var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor()); var metadataProvider = new EmptyModelMetadataProvider(); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/RenderModelBinderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/RenderModelBinderTests.cs index 501c10551d..660a9b7bd1 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/RenderModelBinderTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/RenderModelBinderTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -9,7 +9,9 @@ using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Common.ModelBinders; +using Umbraco.Web.Common.Routing; using Umbraco.Web.Models; +using Umbraco.Web.Website.Routing; namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders { @@ -106,8 +108,9 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders [Test] public void No_DataToken_Returns_Null() { - var content = new MyContent(Mock.Of()); - var bindingContext = CreateBindingContext(typeof(ContentModel), false, content); + IPublishedContent pc = Mock.Of(); + var content = new MyContent(pc); + var bindingContext = CreateBindingContext(typeof(ContentModel), pc, false, content); _contentModelBinder.BindModelAsync(bindingContext); @@ -117,7 +120,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders [Test] public void Invalid_DataToken_Model_Type_Returns_Null() { - var bindingContext = CreateBindingContext(typeof(IPublishedContent), source: "Hello"); + IPublishedContent pc = Mock.Of(); + var bindingContext = CreateBindingContext(typeof(IPublishedContent), pc, source: "Hello"); _contentModelBinder.BindModelAsync(bindingContext); Assert.IsNull(bindingContext.Result.Model); } @@ -125,20 +129,23 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders [Test] public void IPublishedContent_DataToken_Model_Type_Uses_DefaultImplementation() { - var content = new MyContent(Mock.Of()); - var bindingContext = CreateBindingContext(typeof(MyContent), source: content); + IPublishedContent pc = Mock.Of(); + var content = new MyContent(pc); + var bindingContext = CreateBindingContext(typeof(MyContent), pc, source: content); _contentModelBinder.BindModelAsync(bindingContext); Assert.AreEqual(content, bindingContext.Result.Model); } - private ModelBindingContext CreateBindingContext(Type modelType, bool withUmbracoDataToken = true, object source = null) + private ModelBindingContext CreateBindingContext(Type modelType, IPublishedContent publishedContent, bool withUmbracoDataToken = true, object source = null) { var httpContext = new DefaultHttpContext(); var routeData = new RouteData(); if (withUmbracoDataToken) - routeData.DataTokens.Add(Constants.Web.UmbracoDataToken, source); + { + routeData.Values.Add(Constants.Web.UmbracoRouteDefinitionDataToken, new UmbracoRouteValues(publishedContent)); + } var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor()); var metadataProvider = new EmptyModelMetadataProvider(); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttributeTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttributeTests.cs deleted file mode 100644 index bf5c422bd8..0000000000 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttributeTests.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.ViewEngines; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Moq; -using NUnit.Framework; -using Umbraco.Web.Models; -using Umbraco.Web.Mvc; -using Umbraco.Web.Website.Controllers; - -namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Controllers -{ - [TestFixture] - public class RenderIndexActionSelectorAttributeTests - { - [Test] - public void IsValidForRequest__ensure_caching_works() - { - var sut = new RenderIndexActionSelectorAttribute(); - - var actionDescriptor = - GetRenderMvcControllerIndexMethodFromCurrentType(typeof(MatchesDefaultIndexController)).First(); - var actionDescriptorCollectionProviderMock = new Mock(); - actionDescriptorCollectionProviderMock.Setup(x => x.ActionDescriptors) - .Returns(new ActionDescriptorCollection(Array.Empty(), 1)); - - var routeContext = CreateRouteContext(actionDescriptorCollectionProviderMock.Object); - - // Call the method multiple times - for (var i = 0; i < 1; i++) - { - sut.IsValidForRequest(routeContext, actionDescriptor); - } - - //Ensure the underlying ActionDescriptors is only called once. - actionDescriptorCollectionProviderMock.Verify(x=>x.ActionDescriptors, Times.Once); - } - - [Test] - [TestCase(typeof(MatchesDefaultIndexController), - "Index", new[] { typeof(ContentModel) }, typeof(IActionResult), ExpectedResult = true)] - [TestCase(typeof(MatchesOverriddenIndexController), - "Index", new[] { typeof(ContentModel) }, typeof(IActionResult), ExpectedResult = true)] - [TestCase(typeof(MatchesCustomIndexController), - "Index", new[] { typeof(ContentModel), typeof(int) }, typeof(IActionResult), ExpectedResult = false)] - [TestCase(typeof(MatchesAsyncIndexController), - "Index", new[] { typeof(ContentModel) }, typeof(Task), ExpectedResult = false)] - public bool IsValidForRequest__must_return_the_expected_result(Type controllerType, string actionName, Type[] parameterTypes, Type returnType) - { - //Fake all IActionDescriptor's that will be returned by IActionDescriptorCollectionProvider - var actionDescriptors = GetRenderMvcControllerIndexMethodFromCurrentType(controllerType); - - // Find the one that match the current request - var actualActionDescriptor = actionDescriptors.Single(x => x.ActionName == actionName - && x.ControllerTypeInfo.Name == controllerType.Name - && x.MethodInfo.ReturnType == returnType - && x.MethodInfo.GetParameters().Select(m => m.ParameterType).SequenceEqual(parameterTypes)); - - //Fake the IActionDescriptorCollectionProvider and add it to the service collection on httpcontext - var sut = new RenderIndexActionSelectorAttribute(); - - var routeContext = CreateRouteContext(new TestActionDescriptorCollectionProvider(actionDescriptors)); - - //Act - var result = sut.IsValidForRequest(routeContext, actualActionDescriptor); - return result; - } - - private ControllerActionDescriptor[] GetRenderMvcControllerIndexMethodFromCurrentType(Type controllerType) - { - var actions = controllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance) - .Where(m => !m.IsSpecialName - && m.GetCustomAttribute() is null - && m.Module.Name.Contains("Umbraco")); - - var actionDescriptors = actions - .Select(x => new ControllerActionDescriptor() - { - ControllerTypeInfo = controllerType.GetTypeInfo(), - ActionName = x.Name, - MethodInfo = x - }).ToArray(); - - return actionDescriptors; - } - - private static RouteContext CreateRouteContext(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider) - { - //Fake the IActionDescriptorCollectionProvider and add it to the service collection on httpcontext - var httpContext = new DefaultHttpContext(); - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(actionDescriptorCollectionProvider); - httpContext.RequestServices = - new DefaultServiceProviderFactory(new ServiceProviderOptions()) - .CreateServiceProvider(serviceCollection); - - // Put the fake httpcontext on the route context. - var routeContext = new RouteContext(httpContext); - return routeContext; - } - - private class TestActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider - { - public TestActionDescriptorCollectionProvider(IReadOnlyList items) - { - ActionDescriptors = new ActionDescriptorCollection(items, 1); - } - - public ActionDescriptorCollection ActionDescriptors { get; } - } - - private class MatchesDefaultIndexController : RenderController - { - public MatchesDefaultIndexController(ILogger logger, - ICompositeViewEngine compositeViewEngine) : base(logger, compositeViewEngine) - { - } - } - - private class MatchesOverriddenIndexController : RenderController - { - public override IActionResult Index(ContentModel model) - { - return base.Index(model); - } - - public MatchesOverriddenIndexController(ILogger logger, - ICompositeViewEngine compositeViewEngine) : base(logger, compositeViewEngine) - { - } - } - - private class MatchesCustomIndexController : RenderController - { - public IActionResult Index(ContentModel model, int page) - { - return base.Index(model); - } - - public MatchesCustomIndexController(ILogger logger, - ICompositeViewEngine compositeViewEngine) : base(logger, compositeViewEngine) - { - } - } - - private class MatchesAsyncIndexController : RenderController - { - public new async Task Index(ContentModel model) - { - return await Task.FromResult(base.Index(model)); - } - - public MatchesAsyncIndexController(ILogger logger, - ICompositeViewEngine compositeViewEngine) : base(logger, compositeViewEngine) - { - } - } - } -} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs index d1ffc2044e..5db2d435c9 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs @@ -14,6 +14,7 @@ using Umbraco.Core.Services; using Umbraco.Tests.Common; using Umbraco.Tests.Testing; using Umbraco.Web; +using Umbraco.Web.Common.Routing; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; using Umbraco.Web.Security; @@ -164,16 +165,10 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Controllers var content = Mock.Of(publishedContent => publishedContent.Id == 12345); - var publishedRequestMock = new Mock(); - publishedRequestMock.Setup(x => x.PublishedContent).Returns(content); - - var routeDefinition = new RouteDefinition - { - PublishedRequest = publishedRequestMock.Object - }; + var routeDefinition = new UmbracoRouteValues(content); var routeData = new RouteData(); - routeData.DataTokens.Add(CoreConstants.Web.UmbracoRouteDefinitionDataToken, routeDefinition); + routeData.Values.Add(CoreConstants.Web.UmbracoRouteDefinitionDataToken, routeDefinition); var ctrl = new TestSurfaceController(umbracoContextAccessor, Mock.Of(), Mock.Of()); ctrl.ControllerContext = new ControllerContext() diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs index 2bc89e0842..9c9e2d1da2 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Core; @@ -161,7 +161,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache var domainCache = new DomainCache(_domainService, _defaultCultureAccessor); return new PublishedSnapshot( - new PublishedContentCache(_xmlStore, domainCache, _requestCache, _globalSettings, _contentTypeCache, _routesCache,_variationContextAccessor, previewToken), + new PublishedContentCache(_xmlStore, domainCache, _requestCache, _globalSettings, _contentTypeCache, _routesCache, _variationContextAccessor, previewToken), new PublishedMediaCache(_xmlStore, _mediaService, _userService, _requestCache, _contentTypeCache, _entitySerializer, _umbracoContextAccessor, _variationContextAccessor), new PublishedMemberCache(_xmlStore, _requestCache, _memberService, _contentTypeCache, _userService, _variationContextAccessor), domainCache); @@ -278,5 +278,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache { return "Test status"; } + + public override void LoadCachesOnStartup() { } } } diff --git a/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs b/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs index 4e1540a525..e663996d60 100644 --- a/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; @@ -35,11 +35,11 @@ namespace Umbraco.Tests.Scoping DoThing2 = null; DoThing3 = null; - var services = TestHelper.GetRegister(); + var services = TestHelper.GetRegister(); var composition = new UmbracoBuilder(services, Mock.Of(), TestHelper.GetMockedTypeLoader()); - _testObjects = new TestObjects(services); + _testObjects = new TestObjects(); var globalSettings = new GlobalSettings(); composition.Services.AddUnique(factory => new FileSystems(factory, factory.GetService>(), factory.GetService(), TestHelper.IOHelper, Microsoft.Extensions.Options.Options.Create(globalSettings), TestHelper.GetHostingEnvironment())); @@ -47,7 +47,7 @@ namespace Umbraco.Tests.Scoping Current.Factory = composition.CreateServiceProvider(); } - + [TestCase(false, true, true)] [TestCase(false, true, false)] [TestCase(false, false, true)] @@ -140,7 +140,7 @@ namespace Umbraco.Tests.Scoping { //content1 will be filtered from the args - scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(new[]{ content1 , content3})); + scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(new[] { content1, content3 })); scope.Events.Dispatch(DoDeleteForContent, this, new DeleteEventArgs(content1), "DoDeleteForContent"); scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content2)); //this entire event will be filtered @@ -156,7 +156,7 @@ namespace Umbraco.Tests.Scoping Assert.AreEqual(content3.Id, ((SaveEventArgs)events[0].Args).SavedEntities.First().Id); Assert.AreEqual(typeof(DeleteEventArgs), events[1].Args.GetType()); - Assert.AreEqual(content1.Id, ((DeleteEventArgs) events[1].Args).DeletedEntities.First().Id); + Assert.AreEqual(content1.Id, ((DeleteEventArgs)events[1].Args).DeletedEntities.First().Id); Assert.AreEqual(typeof(SaveEventArgs), events[2].Args.GetType()); Assert.AreEqual(content2.Id, ((SaveEventArgs)events[2].Args).SavedEntities.First().Id); @@ -177,8 +177,8 @@ namespace Umbraco.Tests.Scoping var scopeProvider = _testObjects.GetScopeProvider(NullLoggerFactory.Instance); using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) { - scope.Events.Dispatch(Test_Unpublished, contentService, new PublishEventArgs(new [] { content }), "Unpublished"); - scope.Events.Dispatch(Test_Deleted, contentService, new DeleteEventArgs(new [] { content }), "Deleted"); + scope.Events.Dispatch(Test_Unpublished, contentService, new PublishEventArgs(new[] { content }), "Unpublished"); + scope.Events.Dispatch(Test_Deleted, contentService, new DeleteEventArgs(new[] { content }), "Deleted"); // see U4-10764 var events = scope.Events.GetEvents(EventDefinitionFilter.All).ToArray(); @@ -258,9 +258,9 @@ namespace Umbraco.Tests.Scoping // events have been queued var events = scope.Events.GetEvents(EventDefinitionFilter.FirstIn).ToArray(); Assert.AreEqual(1, events.Length); - Assert.AreEqual(content1, ((SaveEventArgs) events[0].Args).SavedEntities.First()); + Assert.AreEqual(content1, ((SaveEventArgs)events[0].Args).SavedEntities.First()); Assert.IsTrue(object.ReferenceEquals(content1, ((SaveEventArgs)events[0].Args).SavedEntities.First())); - Assert.AreEqual(content1.UpdateDate, ((SaveEventArgs) events[0].Args).SavedEntities.First().UpdateDate); + Assert.AreEqual(content1.UpdateDate, ((SaveEventArgs)events[0].Args).SavedEntities.First().UpdateDate); } } diff --git a/src/Umbraco.Tests/TestHelpers/BaseUsingSqlCeSyntax.cs b/src/Umbraco.Tests/TestHelpers/BaseUsingSqlCeSyntax.cs index 6e27bdd07c..fc3b0d6dba 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseUsingSqlCeSyntax.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseUsingSqlCeSyntax.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -27,7 +27,7 @@ namespace Umbraco.Tests.TestHelpers protected ISqlContext SqlContext { get; private set; } - internal TestObjects TestObjects = new TestObjects(null); + internal TestObjects TestObjects = new TestObjects(); protected Sql Sql() { diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index 91b82caccb..3b17861fb3 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -1,7 +1,5 @@ -using System; +using System; using System.Configuration; -using System.IO; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; @@ -9,20 +7,14 @@ using Moq; using NPoco; using Umbraco.Core; using Umbraco.Core.Cache; -using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Events; -using Umbraco.Core.Hosting; using Umbraco.Core.IO; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Scoping; using Umbraco.Persistance.SqlCe; -using Umbraco.Tests.Common.Builders; -using Umbraco.Tests.TestHelpers.Stubs; using Current = Umbraco.Web.Composing.Current; namespace Umbraco.Tests.TestHelpers @@ -32,11 +24,9 @@ namespace Umbraco.Tests.TestHelpers /// internal partial class TestObjects { - private readonly IServiceCollection _register; - public TestObjects(IServiceCollection register) + public TestObjects() { - _register = register; } /// @@ -69,19 +59,7 @@ namespace Umbraco.Tests.TestHelpers return new UmbracoDatabase(connection, sqlContext, logger, TestHelper.BulkSqlInsertProvider); } - private Lazy GetLazyService(IServiceProvider container, Func ctor) - where T : class - { - return new Lazy(() => container?.GetService() ?? ctor(container)); - } - - private T GetRepo(IServiceProvider container) - where T : class, IRepository - { - return container?.GetService() ?? Mock.Of(); - } - - public IScopeProvider GetScopeProvider(ILoggerFactory loggerFactory, ITypeFinder typeFinder = null, FileSystems fileSystems = null, IUmbracoDatabaseFactory databaseFactory = null) + public IScopeProvider GetScopeProvider(ILoggerFactory loggerFactory, FileSystems fileSystems = null, IUmbracoDatabaseFactory databaseFactory = null) { var globalSettings = new GlobalSettings(); var connectionString = ConfigurationManager.ConnectionStrings[Constants.System.UmbracoConnectionName].ConnectionString; @@ -103,11 +81,10 @@ namespace Umbraco.Tests.TestHelpers TestHelper.DbProviderFactoryCreator); } - typeFinder ??= new TypeFinder(loggerFactory.CreateLogger(), new DefaultUmbracoAssemblyProvider(GetType().Assembly), new VaryingRuntimeHash()); fileSystems ??= new FileSystems(Current.Factory, loggerFactory.CreateLogger(), loggerFactory, TestHelper.IOHelper, Options.Create(globalSettings), TestHelper.GetHostingEnvironment()); var coreDebug = TestHelper.CoreDebugSettings; var mediaFileSystem = Mock.Of(); - return new ScopeProvider(databaseFactory, fileSystems, Microsoft.Extensions.Options.Options.Create(coreDebugSettings), mediaFileSystem, loggerFactory.CreateLogger(), loggerFactory, typeFinder, NoAppCache.Instance); + return new ScopeProvider(databaseFactory, fileSystems, Options.Create(coreDebugSettings), mediaFileSystem, loggerFactory.CreateLogger(), loggerFactory, NoAppCache.Instance); } } diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index 533966f5a9..a3d48e3592 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -1,4 +1,4 @@ -using Examine; +using Examine; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -235,10 +235,7 @@ namespace Umbraco.Tests.Testing services.AddUnique(membershipHelper); - - - - TestObjects = new TestObjects(services); + TestObjects = new TestObjects(); Compose(); Current.Factory = Factory = Builder.CreateServiceProvider(); Initialize(); @@ -494,7 +491,7 @@ namespace Umbraco.Tests.Testing Builder.WithCollectionBuilder(); // empty Builder.Services.AddUnique(factory - => TestObjects.GetScopeProvider(_loggerFactory, factory.GetService(), factory.GetService(), factory.GetService())); + => TestObjects.GetScopeProvider(_loggerFactory, factory.GetService(), factory.GetService())); Builder.Services.AddUnique(factory => (IScopeAccessor)factory.GetRequiredService()); Builder.ComposeServices(); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 74eed99143..2f9ab25d2a 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -275,7 +275,6 @@ - diff --git a/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs b/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs index a2bcb4928a..6afc75e931 100644 --- a/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Web; using System.Web.Mvc; using System.Web.Routing; @@ -161,7 +161,7 @@ namespace Umbraco.Tests.Web.Mvc }; var routeData = new RouteData(); - routeData.DataTokens.Add(Core.Constants.Web.UmbracoRouteDefinitionDataToken, routeDefinition); + routeData.Values.Add(Core.Constants.Web.UmbracoRouteDefinitionDataToken, routeDefinition); var ctrl = new TestSurfaceController(umbracoContextAccessor, Mock.Of()); ctrl.ControllerContext = new ControllerContext(Mock.Of(), routeData, ctrl); diff --git a/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs b/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs deleted file mode 100644 index 4e52617e6c..0000000000 --- a/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System.IO; -using System.Web; -using System.Web.Mvc; -using System.Web.Routing; -using Moq; -using NUnit.Framework; -using Umbraco.Core.Security; -using Umbraco.Tests.Common; -using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.Testing; -using Umbraco.Web; -using Umbraco.Web.Mvc; -using Umbraco.Web.PublishedCache; -using Current = Umbraco.Web.Composing.Current; - -namespace Umbraco.Tests.Web -{ - [TestFixture] - [UmbracoTest(WithApplication = true)] - public class WebExtensionMethodTests : UmbracoTestBase - { - [Test] - public void RouteDataExtensions_GetUmbracoContext() - { - var httpContextAccessor = TestHelper.GetHttpContextAccessor(); - - var umbCtx = new UmbracoContext( - httpContextAccessor, - Mock.Of(), - Mock.Of(), - TestObjects.GetGlobalSettings(), - HostingEnvironment, - new TestVariationContextAccessor(), - UriUtility, - new AspNetCookieManager(httpContextAccessor)); - var r1 = new RouteData(); - r1.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, umbCtx); - - Assert.IsTrue(r1.DataTokens.ContainsKey(Core.Constants.Web.UmbracoContextDataToken)); - Assert.AreSame(umbCtx, r1.DataTokens[Core.Constants.Web.UmbracoContextDataToken]); - } - - [Test] - public void ControllerContextExtensions_GetUmbracoContext_From_RouteValues() - { - var httpContextAccessor = TestHelper.GetHttpContextAccessor(); - - var umbCtx = new UmbracoContext( - httpContextAccessor, - Mock.Of(), - Mock.Of(), - TestObjects.GetGlobalSettings(), - HostingEnvironment, - new TestVariationContextAccessor(), - UriUtility, - new AspNetCookieManager(httpContextAccessor)); - - var r1 = new RouteData(); - r1.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, umbCtx); - var ctx1 = CreateViewContext(new ControllerContext(Mock.Of(), r1, new MyController())); - var r2 = new RouteData(); - r2.DataTokens.Add("ParentActionViewContext", ctx1); - var ctx2 = CreateViewContext(new ControllerContext(Mock.Of(), r2, new MyController())); - var r3 = new RouteData(); - r3.DataTokens.Add("ParentActionViewContext", ctx2); - var ctx3 = CreateViewContext(new ControllerContext(Mock.Of(), r3, new MyController())); - - var result = ctx3.GetUmbracoContext(); - - Assert.IsNotNull(result); - Assert.AreSame(umbCtx, result); - } - - [Test] - public void ControllerContextExtensions_GetUmbracoContext_From_Current() - { - var httpContextAccessor = TestHelper.GetHttpContextAccessor(); - - var umbCtx = new UmbracoContext( - httpContextAccessor, - Mock.Of(), - Mock.Of(), - TestObjects.GetGlobalSettings(), - HostingEnvironment, - new TestVariationContextAccessor(), - UriUtility, - new AspNetCookieManager(httpContextAccessor)); - - var httpContext = Mock.Of(); - - var r1 = new RouteData(); - var ctx1 = CreateViewContext(new ControllerContext(httpContext, r1, new MyController())); - var r2 = new RouteData(); - r2.DataTokens.Add("ParentActionViewContext", ctx1); - var ctx2 = CreateViewContext(new ControllerContext(httpContext, r2, new MyController())); - var r3 = new RouteData(); - r3.DataTokens.Add("ParentActionViewContext", ctx2); - var ctx3 = CreateViewContext(new ControllerContext(httpContext, r3, new MyController())); - - Current.UmbracoContextAccessor = new TestUmbracoContextAccessor(); - Current.UmbracoContextAccessor.UmbracoContext = umbCtx; - - var result = ctx3.GetUmbracoContext(); - - Assert.IsNotNull(result); - Assert.AreSame(umbCtx, result); - } - - [Test] - public void ControllerContextExtensions_GetDataTokenInViewContextHierarchy() - { - var r1 = new RouteData(); - r1.DataTokens.Add("hello", "world"); - r1.DataTokens.Add("r", "1"); - var ctx1 = CreateViewContext(new ControllerContext(Mock.Of(), r1, new MyController())); - var r2 = new RouteData(); - r2.DataTokens.Add("ParentActionViewContext", ctx1); - r2.DataTokens.Add("r", "2"); - var ctx2 = CreateViewContext(new ControllerContext(Mock.Of(), r2, new MyController())); - var r3 = new RouteData(); - r3.DataTokens.Add("ParentActionViewContext", ctx2); - r3.DataTokens.Add("r", "3"); - var ctx3 = CreateViewContext(new ControllerContext(Mock.Of(), r3, new MyController())); - - var result = ctx3.GetDataTokenInViewContextHierarchy("hello"); - - Assert.IsNotNull(result as string); - Assert.AreEqual((string) result, "world"); - } - - private static ViewContext CreateViewContext(ControllerContext ctx) - { - return new ViewContext(ctx, Mock.Of(), new ViewDataDictionary(), new TempDataDictionary(), new StringWriter()); - } - - private class MyController : Controller - { } - } -} diff --git a/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs b/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs index 4b8f730e45..a97b67a900 100644 --- a/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs +++ b/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Html; @@ -19,6 +19,7 @@ using Umbraco.Web.Common.ModelBinders; namespace Umbraco.Web.Common.AspNetCore { + // TODO: Should be in Views namespace? public abstract class UmbracoViewPage : UmbracoViewPage { @@ -92,6 +93,8 @@ namespace Umbraco.Web.Common.AspNetCore base.WriteLiteral(value); } + // TODO: This trick doesn't work anymore, this method used to be an override. + // Now the model is bound in a different place // maps model protected async Task SetViewDataAsync(ViewDataDictionary viewData) { @@ -111,8 +114,6 @@ namespace Umbraco.Web.Common.AspNetCore ViewData = (ViewDataDictionary) viewData; } - - // viewData is the ViewDataDictionary (maybe ) that we have // modelType is the type of the model that we need to bind to // diff --git a/src/Umbraco.Web.Website/Controllers/RenderController.cs b/src/Umbraco.Web.Common/Controllers/RenderController.cs similarity index 63% rename from src/Umbraco.Web.Website/Controllers/RenderController.cs rename to src/Umbraco.Web.Common/Controllers/RenderController.cs index 071560d860..ec73c061e2 100644 --- a/src/Umbraco.Web.Website/Controllers/RenderController.cs +++ b/src/Umbraco.Web.Common/Controllers/RenderController.cs @@ -2,13 +2,14 @@ using System; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.Extensions.Logging; +using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Filters; +using Umbraco.Web.Common.Routing; using Umbraco.Web.Models; using Umbraco.Web.Routing; -namespace Umbraco.Web.Website.Controllers +namespace Umbraco.Web.Common.Controllers { /// @@ -17,9 +18,9 @@ namespace Umbraco.Web.Website.Controllers [TypeFilter(typeof(ModelBindingExceptionFilter))] public class RenderController : UmbracoController, IRenderController { - private IPublishedRequest _publishedRequest; private readonly ILogger _logger; private readonly ICompositeViewEngine _compositeViewEngine; + private UmbracoRouteValues _routeValues; /// /// Initializes a new instance of the class. @@ -33,27 +34,27 @@ namespace Umbraco.Web.Website.Controllers /// /// Gets the current content item. /// - protected IPublishedContent CurrentPage => PublishedRequest.PublishedContent; + protected IPublishedContent CurrentPage => UmbracoRouteValues.PublishedContent; /// - /// Gets the current published content request. + /// Gets the /// - protected internal virtual IPublishedRequest PublishedRequest + protected UmbracoRouteValues UmbracoRouteValues { get { - if (_publishedRequest != null) + if (_routeValues != null) { - return _publishedRequest; + return _routeValues; } - if (RouteData.DataTokens.ContainsKey(Core.Constants.Web.PublishedDocumentRequestDataToken) == false) + if (!ControllerContext.RouteData.Values.TryGetValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken, out var def)) { - throw new InvalidOperationException("DataTokens must contain an 'umbraco-doc-request' key with a PublishedRequest object"); + throw new InvalidOperationException($"No route value found with key {Core.Constants.Web.UmbracoRouteDefinitionDataToken}"); } - _publishedRequest = (IPublishedRequest)RouteData.DataTokens[Core.Constants.Web.PublishedDocumentRequestDataToken]; - return _publishedRequest; + _routeValues = (UmbracoRouteValues)def; + return _routeValues; } } @@ -79,22 +80,20 @@ namespace Umbraco.Web.Website.Controllers /// The type of the model. /// The model. /// The action result. - /// If the template found in the route values doesn't physically exist, then an empty ContentResult will be returned. + /// If the template found in the route values doesn't physically exist and exception is thrown protected IActionResult CurrentTemplate(T model) { - var template = ControllerContext.RouteData.Values["action"].ToString(); - if (EnsurePhsyicalViewExists(template) == false) + if (EnsurePhsyicalViewExists(UmbracoRouteValues.TemplateName) == false) { - throw new InvalidOperationException("No physical template file was found for template " + template); + throw new InvalidOperationException("No physical template file was found for template " + UmbracoRouteValues.TemplateName); } - return View(template, model); + return View(UmbracoRouteValues.TemplateName, model); } /// /// The default action to render the front-end view. /// - [RenderIndexActionSelector] - public virtual IActionResult Index(ContentModel model) => CurrentTemplate(model); + public virtual IActionResult Index() => CurrentTemplate(new ContentModel(CurrentPage)); } } diff --git a/src/Umbraco.Web.Common/Macros/MacroRenderer.cs b/src/Umbraco.Web.Common/Macros/MacroRenderer.cs index c79c5229fc..9a9e46eefc 100644 --- a/src/Umbraco.Web.Common/Macros/MacroRenderer.cs +++ b/src/Umbraco.Web.Common/Macros/MacroRenderer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -38,7 +38,7 @@ namespace Umbraco.Web.Macros private readonly IHttpContextAccessor _httpContextAccessor; public MacroRenderer( - IProfilingLogger profilingLogger , + IProfilingLogger profilingLogger, ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, @@ -53,7 +53,7 @@ namespace Umbraco.Web.Macros IRequestAccessor requestAccessor, IHttpContextAccessor httpContextAccessor) { - _profilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger )); + _profilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger)); _logger = logger; _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); _backOfficeSecurityAccessor = backOfficeSecurityAccessor; @@ -111,12 +111,14 @@ namespace Umbraco.Web.Macros private MacroContent GetMacroContentFromCache(MacroModel model) { // only if cache is enabled - if (_umbracoContextAccessor.UmbracoContext.InPreviewMode || model.CacheDuration <= 0) return null; + if (_umbracoContextAccessor.UmbracoContext.InPreviewMode || model.CacheDuration <= 0) + return null; var cache = _appCaches.RuntimeCache; var macroContent = cache.GetCacheItem(CacheKeys.MacroContentCacheKey + model.CacheIdentifier); - if (macroContent == null) return null; + if (macroContent == null) + return null; _logger.LogDebug("Macro content loaded from cache '{MacroCacheId}'", model.CacheIdentifier); @@ -145,16 +147,19 @@ namespace Umbraco.Web.Macros private void AddMacroContentToCache(MacroModel model, MacroContent macroContent) { // only if cache is enabled - if (_umbracoContextAccessor.UmbracoContext.InPreviewMode || model.CacheDuration <= 0) return; + if (_umbracoContextAccessor.UmbracoContext.InPreviewMode || model.CacheDuration <= 0) + return; // just make sure... - if (macroContent == null) return; + if (macroContent == null) + return; // do not cache if it should cache by member and there's not member if (model.CacheByMember) { var key = _memberUserKeyProvider.GetMemberProviderUserKey(); - if (key is null) return; + if (key is null) + return; } // remember when we cache the content @@ -184,10 +189,12 @@ namespace Umbraco.Web.Macros private FileInfo GetMacroFile(MacroModel model) { var filename = GetMacroFileName(model); - if (filename == null) return null; + if (filename == null) + return null; var mapped = _hostingEnvironment.MapPathContentRoot(filename); - if (mapped == null) return null; + if (mapped == null) + return null; var file = new FileInfo(mapped); return file.Exists ? file : null; @@ -223,7 +230,8 @@ namespace Umbraco.Web.Macros private MacroContent Render(MacroModel macro, IPublishedContent content) { - if (content == null) throw new ArgumentNullException(nameof(content)); + if (content == null) + throw new ArgumentNullException(nameof(content)); var macroInfo = $"Render Macro: {macro.Name}, cache: {macro.CacheDuration}"; using (_profilingLogger.DebugDuration(macroInfo, "Rendered Macro.")) @@ -328,7 +336,8 @@ namespace Umbraco.Web.Macros /// should not be cached. In that case the attempt may also contain an exception. private Attempt ExecuteMacroOfType(MacroModel model, IPublishedContent content) { - if (model == null) throw new ArgumentNullException(nameof(model)); + if (model == null) + throw new ArgumentNullException(nameof(model)); // ensure that we are running against a published node (ie available in XML) // that may not be the case if the macro is embedded in a RTE of an unpublished document @@ -356,7 +365,7 @@ namespace Umbraco.Web.Macros /// The text output of the macro execution. private MacroContent ExecutePartialView(MacroModel macro, IPublishedContent content) { - var engine = new PartialViewMacroEngine(_umbracoContextAccessor, _httpContextAccessor, _hostingEnvironment); + var engine = new PartialViewMacroEngine(_httpContextAccessor, _hostingEnvironment); return engine.Execute(macro, content); } diff --git a/src/Umbraco.Web.Common/Macros/PartialViewMacroEngine.cs b/src/Umbraco.Web.Common/Macros/PartialViewMacroEngine.cs index db1658e962..790e437148 100644 --- a/src/Umbraco.Web.Common/Macros/PartialViewMacroEngine.cs +++ b/src/Umbraco.Web.Common/Macros/PartialViewMacroEngine.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text.Encodings.Web; @@ -27,56 +27,71 @@ namespace Umbraco.Web.Common.Macros { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHostingEnvironment _hostingEnvironment; - private readonly Func _getUmbracoContext; + //private readonly Func _getUmbracoContext; public PartialViewMacroEngine( - IUmbracoContextAccessor umbracoContextAccessor, + //IUmbracoContextAccessor umbracoContextAccessor, IHttpContextAccessor httpContextAccessor, IHostingEnvironment hostingEnvironment) { _httpContextAccessor = httpContextAccessor; _hostingEnvironment = hostingEnvironment; - _getUmbracoContext = () => - { - var context = umbracoContextAccessor.UmbracoContext; - if (context == null) - throw new InvalidOperationException( - $"The {GetType()} cannot execute with a null UmbracoContext.Current reference."); - return context; - }; + //_getUmbracoContext = () => + //{ + // var context = umbracoContextAccessor.UmbracoContext; + // if (context == null) + // { + // throw new InvalidOperationException( + // $"The {GetType()} cannot execute with a null UmbracoContext.Current reference."); + // } + + // return context; + //}; } - public bool Validate(string code, string tempFileName, IPublishedContent currentPage, out string errorMessage) - { - var temp = GetVirtualPathFromPhysicalPath(tempFileName); - try - { - CompileAndInstantiate(temp); - } - catch (Exception exception) - { - errorMessage = exception.Message; - return false; - } + //public bool Validate(string code, string tempFileName, IPublishedContent currentPage, out string errorMessage) + //{ + // var temp = GetVirtualPathFromPhysicalPath(tempFileName); + // try + // { + // CompileAndInstantiate(temp); + // } + // catch (Exception exception) + // { + // errorMessage = exception.Message; + // return false; + // } - errorMessage = string.Empty; - return true; - } + // errorMessage = string.Empty; + // return true; + //} public MacroContent Execute(MacroModel macro, IPublishedContent content) { - if (macro == null) throw new ArgumentNullException(nameof(macro)); - if (content == null) throw new ArgumentNullException(nameof(content)); + if (macro == null) + { + throw new ArgumentNullException(nameof(macro)); + } + + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + if (string.IsNullOrWhiteSpace(macro.MacroSource)) + { throw new ArgumentException("The MacroSource property of the macro object cannot be null or empty"); + } var httpContext = _httpContextAccessor.GetRequiredHttpContext(); - var umbCtx = _getUmbracoContext(); + //var umbCtx = _getUmbracoContext(); var routeVals = new RouteData(); routeVals.Values.Add("controller", "PartialViewMacro"); routeVals.Values.Add("action", "Index"); - routeVals.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, umbCtx); //required for UmbracoViewPage + + //TODO: Was required for UmbracoViewPage need to figure out if we still need that, i really don't think this is necessary + //routeVals.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, umbCtx); var modelMetadataProvider = httpContext.RequestServices.GetRequiredService(); var tempDataProvider = httpContext.RequestServices.GetRequiredService(); @@ -109,6 +124,7 @@ namespace Umbraco.Web.Common.Macros return new MacroContent { Text = output }; } + private class FakeView : IView { /// @@ -120,29 +136,30 @@ namespace Umbraco.Web.Common.Macros /// public string Path { get; } = "View"; } - private string GetVirtualPathFromPhysicalPath(string physicalPath) - { - var rootpath = _hostingEnvironment.MapPathContentRoot("~/"); - physicalPath = physicalPath.Replace(rootpath, ""); - physicalPath = physicalPath.Replace("\\", "/"); - return "~/" + physicalPath; - } - private static PartialViewMacroPage CompileAndInstantiate(string virtualPath) - { - // //Compile Razor - We Will Leave This To ASP.NET Compilation Engine & ASP.NET WebPages - // //Security in medium trust is strict around here, so we can only pass a virtual file path - // //ASP.NET Compilation Engine caches returned types - // //Changed From BuildManager As Other Properties Are Attached Like Context Path/ - // var webPageBase = WebPageBase.CreateInstanceFromVirtualPath(virtualPath); - // var webPage = webPageBase as PartialViewMacroPage; - // if (webPage == null) - // throw new InvalidCastException("All Partial View Macro views must inherit from " + typeof(PartialViewMacroPage).FullName); - // return webPage; + //private string GetVirtualPathFromPhysicalPath(string physicalPath) + //{ + // var rootpath = _hostingEnvironment.MapPathContentRoot("~/"); + // physicalPath = physicalPath.Replace(rootpath, ""); + // physicalPath = physicalPath.Replace("\\", "/"); + // return "~/" + physicalPath; + //} - //TODO? How to check this - return null; - } + //private static PartialViewMacroPage CompileAndInstantiate(string virtualPath) + //{ + // // //Compile Razor - We Will Leave This To ASP.NET Compilation Engine & ASP.NET WebPages + // // //Security in medium trust is strict around here, so we can only pass a virtual file path + // // //ASP.NET Compilation Engine caches returned types + // // //Changed From BuildManager As Other Properties Are Attached Like Context Path/ + // // var webPageBase = WebPageBase.CreateInstanceFromVirtualPath(virtualPath); + // // var webPage = webPageBase as PartialViewMacroPage; + // // if (webPage == null) + // // throw new InvalidCastException("All Partial View Macro views must inherit from " + typeof(PartialViewMacroPage).FullName); + // // return webPage; + + // //TODO? How to check this + // return null; + //} } } diff --git a/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs b/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs index 113f411c6f..d8178033c9 100644 --- a/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs +++ b/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ModelBinding; using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Common.Routing; using Umbraco.Web.Models; namespace Umbraco.Web.Common.ModelBinders @@ -15,26 +16,15 @@ namespace Umbraco.Web.Common.ModelBinders { public Task BindModelAsync(ModelBindingContext bindingContext) { - if (bindingContext.ActionContext.RouteData.DataTokens.TryGetValue(Core.Constants.Web.UmbracoDataToken, out var source) == false) + // Although this model binder is built to work both ways between IPublishedContent and IContentModel in reality + // only IPublishedContent will ever exist in the request. + if (!bindingContext.ActionContext.RouteData.Values.TryGetValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken, out var source) + || !(source is UmbracoRouteValues umbracoRouteValues)) { return Task.CompletedTask; } - // This model binder deals with IContentModel and IPublishedContent by extracting the model from the route's - // datatokens. This data token is set in 2 places: RenderRouteHandler, UmbracoVirtualNodeRouteHandler - // and both always set the model to an instance of `ContentModel`. - - // No need for type checks to ensure we have the appropriate binder, as in .NET Core this is handled in the provider, - // in this case ContentModelBinderProvider. - - // Being defensive though.... if for any reason the model is not either IContentModel or IPublishedContent, - // then we return since those are the only types this binder is dealing with. - if (source is IContentModel == false && source is IPublishedContent == false) - { - return Task.CompletedTask; - } - - BindModelAsync(bindingContext, source, bindingContext.ModelType); + BindModelAsync(bindingContext, umbracoRouteValues.PublishedContent, bindingContext.ModelType); return Task.CompletedTask; } @@ -56,7 +46,7 @@ namespace Umbraco.Web.Common.ModelBinders // If types already match, return var sourceType = source.GetType(); - if (sourceType. Inherits(modelType)) // includes == + if (sourceType.Inherits(modelType)) // includes == { bindingContext.Result = ModelBindingResult.Success(source); return Task.CompletedTask; @@ -71,7 +61,8 @@ namespace Umbraco.Web.Common.ModelBinders { // else check if we can convert it to a content var attempt1 = source.TryConvertTo(); - if (attempt1.Success) sourceContent = attempt1.Result; + if (attempt1.Success) + sourceContent = attempt1.Result; } // If we have a content @@ -129,11 +120,13 @@ namespace Umbraco.Web.Common.ModelBinders // prepare message msg.Append("Cannot bind source"); - if (sourceContent) msg.Append(" content"); + if (sourceContent) + msg.Append(" content"); msg.Append(" type "); msg.Append(sourceType.FullName); msg.Append(" to model"); - if (modelContent) msg.Append(" content"); + if (modelContent) + msg.Append(" content"); msg.Append(" type "); msg.Append(modelType.FullName); msg.Append("."); diff --git a/src/Umbraco.Web.Common/Routing/UmbracoRouteValues.cs b/src/Umbraco.Web.Common/Routing/UmbracoRouteValues.cs new file mode 100644 index 0000000000..2ab047a757 --- /dev/null +++ b/src/Umbraco.Web.Common/Routing/UmbracoRouteValues.cs @@ -0,0 +1,68 @@ +using System; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Extensions; +using Umbraco.Web.Common.Controllers; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.Common.Routing +{ + /// + /// Represents the data required to route to a specific controller/action during an Umbraco request + /// + public class UmbracoRouteValues + { + /// + /// The default action name + /// + public const string DefaultActionName = nameof(RenderController.Index); + + /// + /// Initializes a new instance of the class. + /// + public UmbracoRouteValues( + IPublishedContent publishedContent, + string controllerName = null, + Type controllerType = null, + string actionName = DefaultActionName, + string templateName = null, + bool hasHijackedRoute = false) + { + ControllerName = controllerName ?? ControllerExtensions.GetControllerName(); + ControllerType = controllerType ?? typeof(RenderController); + PublishedContent = publishedContent; + HasHijackedRoute = hasHijackedRoute; + ActionName = actionName; + TemplateName = templateName; + } + + /// + /// Gets the controller name + /// + public string ControllerName { get; } + + /// + /// Gets the action name + /// + public string ActionName { get; } + + /// + /// Gets the template name + /// + public string TemplateName { get; } + + /// + /// Gets the Controller type found for routing to + /// + public Type ControllerType { get; } + + /// + /// Gets the + /// + public IPublishedContent PublishedContent { get; } + + /// + /// Gets a value indicating whether the current request has a hijacked route/user controller routed for it + /// + public bool HasHijackedRoute { get; } + } +} diff --git a/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs b/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs index 62942541e9..abf269e062 100644 --- a/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs +++ b/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -99,7 +99,7 @@ namespace Umbraco.Web.Website.ActionResults /// private static void ValidateRouteData(RouteData routeData) { - if (routeData.DataTokens.ContainsKey(Constants.Web.UmbracoRouteDefinitionDataToken) == false) + if (routeData.Values.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"); diff --git a/src/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttribute.cs b/src/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttribute.cs deleted file mode 100644 index 0027132c23..0000000000 --- a/src/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttribute.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.ActionConstraints; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Umbraco.Web.Website.Controllers -{ - /// - /// A custom ActionMethodSelector which will ensure that the RenderMvcController.Index(ContentModel model) action will be executed - /// if the - /// - internal class RenderIndexActionSelectorAttribute : ActionMethodSelectorAttribute - { - private static readonly ConcurrentDictionary> _controllerActionsCache = new ConcurrentDictionary>(); - - /// - /// Determines whether the action method selection is valid for the specified controller context. - /// - /// - /// true if the action method selection is valid for the specified controller context; otherwise, false. - /// - /// The route context. - /// Information about the action method. - public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action) - { - if (action is ControllerActionDescriptor controllerAction) - { - var currType = controllerAction.ControllerTypeInfo.UnderlyingSystemType; - var baseType = controllerAction.ControllerTypeInfo.BaseType; - - //It's the same type, so this must be the Index action to use - if (currType == baseType) - return true; - - var actions = _controllerActionsCache.GetOrAdd(currType, type => - { - var actionDescriptors = routeContext.HttpContext.RequestServices - .GetRequiredService().ActionDescriptors.Items - .Where(x => x is ControllerActionDescriptor).Cast() - .Where(x => x.ControllerTypeInfo == controllerAction.ControllerTypeInfo); - - return actionDescriptors; - }); - - //If there are more than one Index action for this controller, then - // this base class one should not be matched - return actions.Count(x => x.ActionName == "Index") <= 1; - } - - return false; - - } - } -} diff --git a/src/Umbraco.Web.Website/Controllers/SurfaceController.cs b/src/Umbraco.Web.Website/Controllers/SurfaceController.cs index 1d3e4c5626..390da69453 100644 --- a/src/Umbraco.Web.Website/Controllers/SurfaceController.cs +++ b/src/Umbraco.Web.Website/Controllers/SurfaceController.cs @@ -8,9 +8,9 @@ using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Web.Common.Controllers; +using Umbraco.Web.Common.Routing; using Umbraco.Web.Routing; using Umbraco.Web.Website.ActionResults; -using Umbraco.Web.Website.Routing; namespace Umbraco.Web.Website.Controllers { @@ -39,18 +39,18 @@ namespace Umbraco.Web.Website.Controllers { var routeDefAttempt = TryGetRouteDefinitionFromAncestorViewContexts(); if (routeDefAttempt.Success == false) + { throw routeDefAttempt.Exception; + } var routeDef = routeDefAttempt.Result; - return routeDef.PublishedRequest.PublishedContent; + return routeDef.PublishedContent; } } /// /// Redirects to the Umbraco page with the given id /// - /// - /// protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid contentKey) { return new RedirectToUmbracoPageResult(contentKey, PublishedUrlProvider, UmbracoContextAccessor); @@ -59,9 +59,6 @@ namespace Umbraco.Web.Website.Controllers /// /// 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); @@ -70,8 +67,6 @@ namespace Umbraco.Web.Website.Controllers /// /// Redirects to the Umbraco page with the given published content /// - /// - /// protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent) { return new RedirectToUmbracoPageResult(publishedContent, PublishedUrlProvider, UmbracoContextAccessor); @@ -80,9 +75,6 @@ namespace Umbraco.Web.Website.Controllers /// /// 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); @@ -91,7 +83,6 @@ namespace Umbraco.Web.Website.Controllers /// /// Redirects to the currently rendered Umbraco page /// - /// protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage() { return new RedirectToUmbracoPageResult(CurrentPage, PublishedUrlProvider, UmbracoContextAccessor); @@ -100,8 +91,6 @@ namespace Umbraco.Web.Website.Controllers /// /// Redirects to the currently rendered Umbraco page and passes provided querystring /// - /// - /// protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage(QueryString queryString) { return new RedirectToUmbracoPageResult(CurrentPage, queryString, PublishedUrlProvider, UmbracoContextAccessor); @@ -110,7 +99,6 @@ namespace Umbraco.Web.Website.Controllers /// /// Redirects to the currently rendered Umbraco URL /// - /// /// /// 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 @@ -124,7 +112,6 @@ namespace Umbraco.Web.Website.Controllers /// /// Returns the currently rendered Umbraco page /// - /// protected UmbracoPageResult CurrentUmbracoPage() { return new UmbracoPageResult(ProfilingLogger); @@ -133,18 +120,19 @@ namespace Umbraco.Web.Website.Controllers /// /// we need to recursively find the route definition based on the parent view context /// - /// - private Attempt TryGetRouteDefinitionFromAncestorViewContexts() + private Attempt TryGetRouteDefinitionFromAncestorViewContexts() { var currentContext = ControllerContext; while (!(currentContext is null)) { var currentRouteData = currentContext.RouteData; - if (currentRouteData.DataTokens.ContainsKey(Core.Constants.Web.UmbracoRouteDefinitionDataToken)) - return Attempt.Succeed((RouteDefinition)currentRouteData.DataTokens[Core.Constants.Web.UmbracoRouteDefinitionDataToken]); + if (currentRouteData.Values.ContainsKey(Core.Constants.Web.UmbracoRouteDefinitionDataToken)) + { + return Attempt.Succeed((UmbracoRouteValues)currentRouteData.Values[Core.Constants.Web.UmbracoRouteDefinitionDataToken]); + } } - return Attempt.Fail( + return Attempt.Fail( new InvalidOperationException("Cannot find the Umbraco route definition in the route values, the request must be made in the context of an Umbraco request")); } } diff --git a/src/Umbraco.Web.Website/Controllers/UmbracoRenderingDefaults.cs b/src/Umbraco.Web.Website/Controllers/UmbracoRenderingDefaults.cs index 669e1835d4..65c27a3269 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbracoRenderingDefaults.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbracoRenderingDefaults.cs @@ -1,4 +1,5 @@ using System; +using Umbraco.Web.Common.Controllers; namespace Umbraco.Web.Website.Controllers { diff --git a/src/Umbraco.Web.Website/Routing/RouteDefinition.cs b/src/Umbraco.Web.Website/Routing/RouteDefinition.cs deleted file mode 100644 index 47206bd0c3..0000000000 --- a/src/Umbraco.Web.Website/Routing/RouteDefinition.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using Umbraco.Web.Routing; - -namespace Umbraco.Web.Website.Routing -{ - /// - /// 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; } - } -} diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index 328b7cc1d4..2c1debc3ee 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -10,10 +12,12 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using Umbraco.Core; using Umbraco.Core.Composing; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Strings; using Umbraco.Extensions; using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Middleware; +using Umbraco.Web.Common.Routing; using Umbraco.Web.Models; using Umbraco.Web.Routing; using Umbraco.Web.Website.Controllers; @@ -23,6 +27,12 @@ namespace Umbraco.Web.Website.Routing /// /// The route value transformer for Umbraco front-end routes /// + /// + /// NOTE: In aspnet 5 DynamicRouteValueTransformer has been improved, see https://github.com/dotnet/aspnetcore/issues/21471 + /// It seems as though with the "State" parameter we could more easily assign the IPublishedRequest or IPublishedContent + /// or UmbracoContext more easily that way. In the meantime we will rely on assigning the IPublishedRequest to the + /// route values along with the IPublishedContent to the umbraco context + /// public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer { private readonly ILogger _logger; @@ -59,20 +69,13 @@ namespace Umbraco.Web.Website.Routing throw new InvalidOperationException($"There is no current UmbracoContext, it must be initialized before the {nameof(UmbracoRouteValueTransformer)} executes, ensure that {nameof(UmbracoRequestMiddleware)} is registered prior to 'UseRouting'"); } - bool routed = RouteRequest(_umbracoContextAccessor.UmbracoContext); + bool routed = RouteRequest(_umbracoContextAccessor.UmbracoContext, out IPublishedRequest publishedRequest); if (!routed) { // TODO: Deal with it not being routable, perhaps this should be an enum result? } - IPublishedRequest request = _umbracoContextAccessor.UmbracoContext.PublishedRequest; // This cannot be null here - - SetupRouteDataForRequest( - new ContentModel(request.PublishedContent), - request, - values); - - RouteDefinition routeDef = GetUmbracoRouteDefinition(httpContext, values, request); + UmbracoRouteValues routeDef = GetUmbracoRouteDefinition(httpContext, values, publishedRequest); values["controller"] = routeDef.ControllerName; if (string.IsNullOrWhiteSpace(routeDef.ActionName) == false) { @@ -83,29 +86,9 @@ namespace Umbraco.Web.Website.Routing } /// - /// Ensures that all of the correct DataTokens are added to the route values which are all required for rendering front-end umbraco views + /// Returns a object based on the current content request /// - private void SetupRouteDataForRequest(ContentModel contentModel, IPublishedRequest frequest, RouteValueDictionary values) - { - // put essential data into the data tokens, the 'umbraco' key is required to be there for the view engine - - // required for the ContentModelBinder and view engine. - // TODO: Are we sure, seems strange to need this in netcore - values.TryAdd(Constants.Web.UmbracoDataToken, contentModel); - - // required for RenderMvcController - // TODO: Are we sure, seems strange to need this in netcore - values.TryAdd(Constants.Web.PublishedDocumentRequestDataToken, frequest); - - // required for UmbracoViewPage - // TODO: Are we sure, seems strange to need this in netcore - values.TryAdd(Constants.Web.UmbracoContextDataToken, _umbracoContextAccessor.UmbracoContext); - } - - /// - /// Returns a object based on the current content request - /// - private RouteDefinition GetUmbracoRouteDefinition(HttpContext httpContext, RouteValueDictionary values, IPublishedRequest request) + private UmbracoRouteValues GetUmbracoRouteDefinition(HttpContext httpContext, RouteValueDictionary values, IPublishedRequest request) { if (httpContext is null) { @@ -125,17 +108,8 @@ namespace Umbraco.Web.Website.Routing Type defaultControllerType = _renderingDefaults.DefaultControllerType; var defaultControllerName = ControllerExtensions.GetControllerName(defaultControllerType); - // creates the default route definition which maps to the 'UmbracoController' controller - var def = new RouteDefinition - { - ControllerName = defaultControllerName, - ControllerType = defaultControllerType, - PublishedRequest = request, - - // ActionName = ((Route)requestContext.RouteData.Route).Defaults["action"].ToString(), - ActionName = "Index", - HasHijackedRoute = false - }; + string customActionName = null; + var customControllerName = request.PublishedContent.ContentType.Alias; // never null // check that a template is defined), if it doesn't and there is a hijacked route it will just route // to the index Action @@ -144,34 +118,47 @@ namespace Umbraco.Web.Website.Routing // the template Alias should always be already saved with a safe name. // if there are hyphens in the name and there is a hijacked route, then the Action will need to be attributed // with the action name attribute. - var templateName = request.TemplateAlias.Split('.')[0].ToSafeAlias(_shortStringHelper); - def.ActionName = templateName; + customActionName = request.TemplateAlias.Split('.')[0].ToSafeAlias(_shortStringHelper); } - // check if there's a custom controller assigned, base on the document type alias. - Type controllerType = FindControllerType(request.PublishedContent.ContentType.Alias); + // creates the default route definition which maps to the 'UmbracoController' controller + var def = new UmbracoRouteValues( + request.PublishedContent, + defaultControllerName, + defaultControllerType, + templateName: customActionName); - // check if that controller exists - if (controllerType != null) + IReadOnlyList candidates = FindControllerCandidates(customControllerName, customActionName, def.ActionName); + + // check if there's a custom controller assigned, base on the document type alias. + var customControllerCandidates = candidates.Where(x => x.ControllerName.InvariantEquals(customControllerName)).ToList(); + + // check if that custom controller exists + if (customControllerCandidates.Count > 0) { + ControllerActionDescriptor controllerDescriptor = customControllerCandidates[0]; + // ensure the controller is of type IRenderController and ControllerBase - if (TypeHelper.IsTypeAssignableFrom(controllerType) - && TypeHelper.IsTypeAssignableFrom(controllerType)) + if (TypeHelper.IsTypeAssignableFrom(controllerDescriptor.ControllerTypeInfo) + && TypeHelper.IsTypeAssignableFrom(controllerDescriptor.ControllerTypeInfo)) { - // set the controller and name to the custom one - def.ControllerType = controllerType; - def.ControllerName = ControllerExtensions.GetControllerName(controllerType); - if (def.ControllerName != defaultControllerName) - { - def.HasHijackedRoute = true; - } + // now check if the custom action matches + var customActionExists = customActionName != null && customControllerCandidates.Any(x => x.ActionName.InvariantEquals(customActionName)); + + def = new UmbracoRouteValues( + request.PublishedContent, + controllerDescriptor.ControllerName, + controllerDescriptor.ControllerTypeInfo, + customActionExists ? customActionName : def.ActionName, + customActionName, + true); // Hijacked = true } else { _logger.LogWarning( "The current Document Type {ContentTypeAlias} matches a locally declared controller of type {ControllerName}. Custom Controllers for Umbraco routing must implement '{UmbracoRenderController}' and inherit from '{UmbracoControllerBase}'.", request.PublishedContent.ContentType.Alias, - controllerType.FullName, + controllerDescriptor.ControllerTypeInfo.FullName, typeof(IRenderController).FullName, typeof(ControllerBase).FullName); @@ -186,17 +173,21 @@ namespace Umbraco.Web.Website.Routing return def; } - private Type FindControllerType(string controllerName) + /// + /// Return a list of controller candidates that match the custom controller and action names + /// + private IReadOnlyList FindControllerCandidates(string customControllerName, string customActionName, string defaultActionName) { - ControllerActionDescriptor descriptor = _actionDescriptorCollectionProvider.ActionDescriptors.Items + var descriptors = _actionDescriptorCollectionProvider.ActionDescriptors.Items .Cast() - .FirstOrDefault(x => - x.ControllerName.Equals(controllerName)); + .Where(x => x.ControllerName.InvariantEquals(customControllerName) + && (x.ActionName.InvariantEquals(defaultActionName) || (customActionName != null && x.ActionName.InvariantEquals(customActionName)))) + .ToList(); - return descriptor?.ControllerTypeInfo; + return descriptors; } - private bool RouteRequest(IUmbracoContext umbracoContext) + private bool RouteRequest(IUmbracoContext umbracoContext, out IPublishedRequest publishedRequest) { // TODO: I suspect one day this will be async @@ -209,7 +200,9 @@ namespace Umbraco.Web.Website.Routing // instantiate, prepare and process the published content request // important to use CleanedUmbracoUrl - lowercase path-only version of the current url IPublishedRequest request = _publishedRouter.CreateRequest(umbracoContext); - umbracoContext.PublishedRequest = request; // TODO: This is ugly + + // TODO: This is ugly with the re-assignment to umbraco context also because IPublishedRequest is mutable + publishedRequest = umbracoContext.PublishedRequest = request; bool prepared = _publishedRouter.PrepareRequest(request); return prepared && request.HasPublishedContent; diff --git a/src/Umbraco.Web.Website/ViewEngines/PluginViewEngine.cs b/src/Umbraco.Web.Website/ViewEngines/PluginViewEngine.cs index ac16be417a..e0b16a351e 100644 --- a/src/Umbraco.Web.Website/ViewEngines/PluginViewEngine.cs +++ b/src/Umbraco.Web.Website/ViewEngines/PluginViewEngine.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.Logging; @@ -6,6 +6,10 @@ using Microsoft.Extensions.Options; namespace Umbraco.Web.Website.ViewEngines { + // TODO: We don't really need to have different view engines simply to search additional places, + // we can just do ConfigureOptions on startup to add more to the + // default list so this can be totally removed/replaced with configure options logic. + /// /// A view engine to look into the App_Plugins folder for views for packaged controllers /// @@ -21,28 +25,28 @@ namespace Umbraco.Web.Website.ViewEngines { } - private static IOptions OverrideViewLocations() + private static IOptions OverrideViewLocations() => Options.Create(new RazorViewEngineOptions() { - return Options.Create(new RazorViewEngineOptions() - { - AreaViewLocationFormats = + // This is definitely not doing what it used to do :P see: + // https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/src/Umbraco.Web/Mvc/PluginViewEngine.cs#L23 + + AreaViewLocationFormats = { - //set all of the area view locations to the plugin folder + // set all of the area view locations to the plugin folder string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/{1}/{0}.cshtml"), string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/Shared/{0}.cshtml"), - //will be used when we have partial view and child action macros + // will be used when we have partial view and child action macros string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/Partials/{0}.cshtml"), string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/MacroPartials/{0}.cshtml"), - //for partialsCurrent. + // for partialsCurrent. string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/{1}/{0}.cshtml"), string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/Shared/{0}.cshtml"), }, - ViewLocationFormats = + ViewLocationFormats = { string.Concat(Core.Constants.SystemDirectories.AppPlugins, "/{2}/Views/{1}/{0}.cshtml"), } - }); - } + }); } } diff --git a/src/Umbraco.Web.Website/ViewEngines/RenderViewEngine.cs b/src/Umbraco.Web.Website/ViewEngines/RenderViewEngine.cs index ea727a07f1..8c53255928 100644 --- a/src/Umbraco.Web.Website/ViewEngines/RenderViewEngine.cs +++ b/src/Umbraco.Web.Website/ViewEngines/RenderViewEngine.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -16,6 +16,10 @@ using Umbraco.Web.Models; namespace Umbraco.Web.Website.ViewEngines { + // TODO: We don't really need to have different view engines simply to search additional places, + // we can just do ConfigureOptions on startup to add more to the + // default list so this can be totally removed/replaced with configure options logic. + /// /// A view engine to look into the template location specified in the config for the front-end/Rendering part of the cms, /// this includes paths to render partial macros and media item templates. @@ -23,6 +27,9 @@ namespace Umbraco.Web.Website.ViewEngines public class RenderViewEngine : RazorViewEngine, IRenderViewEngine { + /// + /// Initializes a new instance of the class. + /// public RenderViewEngine( IRazorPageFactoryProvider pageFactory, IRazorPageActivator pageActivator, @@ -33,27 +40,24 @@ namespace Umbraco.Web.Website.ViewEngines { } - private static IOptions OverrideViewLocations() + private static IOptions OverrideViewLocations() => Options.Create(new RazorViewEngineOptions() { - return Options.Create(new RazorViewEngineOptions() - { - //NOTE: we will make the main view location the last to be searched since if it is the first to be searched and there is both a view and a partial - // view in both locations and the main view is rendering a partial view with the same name, we will get a stack overflow exception. - // http://issues.umbraco.org/issue/U4-1287, http://issues.umbraco.org/issue/U4-1215 - ViewLocationFormats = + // NOTE: we will make the main view location the last to be searched since if it is the first to be searched and there is both a view and a partial + // view in both locations and the main view is rendering a partial view with the same name, we will get a stack overflow exception. + // http://issues.umbraco.org/issue/U4-1287, http://issues.umbraco.org/issue/U4-1215 + ViewLocationFormats = { - "/Partials/{0}.cshtml", - "/MacroPartials/{0}.cshtml", - "/{0}.cshtml" + "/Views/Partials/{0}.cshtml", + "/Views/MacroPartials/{0}.cshtml", + "/Views/{0}.cshtml" }, - AreaViewLocationFormats = + AreaViewLocationFormats = { - "/Partials/{0}.cshtml", - "/MacroPartials/{0}.cshtml", - "/{0}.cshtml" + "/Views/Partials/{0}.cshtml", + "/Views/MacroPartials/{0}.cshtml", + "/Views/{0}.cshtml" } - }); - } + }); public new ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage) { @@ -72,16 +76,17 @@ namespace Umbraco.Web.Website.ViewEngines /// private static bool ShouldFindView(ActionContext context, bool isMainPage) { - //In v8, this was testing recursively into if it was a child action, but child action do not exist anymore, - //And my best guess is that it - context.RouteData.DataTokens.TryGetValue(Core.Constants.Web.UmbracoDataToken, out var umbracoToken); - // first check if we're rendering a partial view for the back office, or surface controller, etc... - // anything that is not ContentModel as this should only pertain to Umbraco views. - if (!isMainPage && !(umbracoToken is ContentModel)) - return true; - - // only find views if we're rendering the umbraco front end - return umbracoToken is ContentModel; + return true; + // TODO: Determine if this is required, i don't think it is + ////In v8, this was testing recursively into if it was a child action, but child action do not exist anymore, + ////And my best guess is that it + //context.RouteData.DataTokens.TryGetValue(Core.Constants.Web.UmbracoDataToken, out var umbracoToken); + //// first check if we're rendering a partial view for the back office, or surface controller, etc... + //// anything that is not ContentModel as this should only pertain to Umbraco views. + //if (!isMainPage && !(umbracoToken is ContentModel)) + // return true; + //// only find views if we're rendering the umbraco front end + //return umbracoToken is ContentModel; } diff --git a/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs b/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs index 1c2be3f713..c59c701d42 100644 --- a/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs +++ b/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Web.Http; using System.Web.Mvc; @@ -127,7 +127,9 @@ namespace Umbraco.Web.Mvc //match this area controllerPluginRoute.DataTokens.Add("area", area.AreaName); - controllerPluginRoute.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, umbracoTokenValue); //ensure the umbraco token is set + + // TODO: No longer needed, remove + //controllerPluginRoute.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, umbracoTokenValue); //ensure the umbraco token is set return controllerPluginRoute; } diff --git a/src/Umbraco.Web/Mvc/ControllerContextExtensions.cs b/src/Umbraco.Web/Mvc/ControllerContextExtensions.cs deleted file mode 100644 index 4baaaac4fc..0000000000 --- a/src/Umbraco.Web/Mvc/ControllerContextExtensions.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Web.Mvc; -using Umbraco.Web.Composing; - -namespace Umbraco.Web.Mvc -{ - public static class ControllerContextExtensions - { - /// - /// Gets the Umbraco context from a controller context hierarchy, if any, else the 'current' Umbraco context. - /// - /// The controller context. - /// The Umbraco context. - public static IUmbracoContext GetUmbracoContext(this ControllerContext controllerContext) - { - var o = controllerContext.GetDataTokenInViewContextHierarchy(Core.Constants.Web.UmbracoContextDataToken); - return o != null ? o as IUmbracoContext : Current.UmbracoContext; - } - - /// - /// Recursively gets a data token from a controller context hierarchy. - /// - /// The controller context. - /// The name of the data token. - /// The data token, or null. - internal static object GetDataTokenInViewContextHierarchy(this ControllerContext controllerContext, string dataTokenName) - { - var context = controllerContext; - while (context != null) - { - object token; - if (context.RouteData.DataTokens.TryGetValue(dataTokenName, out token)) - return token; - context = context.ParentActionViewContext; - } - return null; - } - } -} diff --git a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs index 19e1b79c89..3ca0931585 100644 --- a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs @@ -56,8 +56,6 @@ namespace Umbraco.Web.Mvc /// Assigns the correct controller based on the Umbraco request and returns a standard MvcHandler to process the response, /// this also stores the render model into the data tokens for the current RouteData. /// - /// - /// public IHttpHandler GetHttpHandler(RequestContext requestContext) { if (UmbracoContext == null) @@ -70,37 +68,18 @@ namespace Umbraco.Web.Mvc throw new NullReferenceException("There is no current PublishedRequest, it must be initialized before the RenderRouteHandler executes"); } - SetupRouteDataForRequest( - new ContentModel(request.PublishedContent), - requestContext, - request); - return GetHandlerForRoute(requestContext, request); } #endregion - /// - /// Ensures that all of the correct DataTokens are added to the route values which are all required for rendering front-end umbraco views - /// - /// - /// - /// - internal void SetupRouteDataForRequest(ContentModel contentModel, RequestContext requestContext, IPublishedRequest frequest) - { - //put essential data into the data tokens, the 'umbraco' key is required to be there for the view engine - requestContext.RouteData.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, contentModel); //required for the ContentModelBinder and view engine - requestContext.RouteData.DataTokens.Add(Core.Constants.Web.PublishedDocumentRequestDataToken, frequest); //required for RenderMvcController - requestContext.RouteData.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, UmbracoContext); //required for UmbracoViewPage - } - private void UpdateRouteDataForRequest(ContentModel contentModel, RequestContext requestContext) { if (contentModel == null) throw new ArgumentNullException(nameof(contentModel)); if (requestContext == null) throw new ArgumentNullException(nameof(requestContext)); - requestContext.RouteData.DataTokens[Core.Constants.Web.UmbracoDataToken] = contentModel; + // requestContext.RouteData.DataTokens[Core.Constants.Web.UmbracoDataToken] = contentModel; // the rest should not change -- it's only the published content that has changed } @@ -293,7 +272,7 @@ namespace Umbraco.Web.Mvc } //store the route definition - requestContext.RouteData.DataTokens[Core.Constants.Web.UmbracoRouteDefinitionDataToken] = def; + requestContext.RouteData.Values[Core.Constants.Web.UmbracoRouteDefinitionDataToken] = def; return def; } diff --git a/src/Umbraco.Web/Mvc/SurfaceController.cs b/src/Umbraco.Web/Mvc/SurfaceController.cs index cd344ea261..fa67248e7d 100644 --- a/src/Umbraco.Web/Mvc/SurfaceController.cs +++ b/src/Umbraco.Web/Mvc/SurfaceController.cs @@ -1,4 +1,4 @@ -using System; +using System; using Umbraco.Core; using System.Collections.Specialized; using Umbraco.Core.Cache; @@ -204,8 +204,10 @@ namespace Umbraco.Web.Mvc while (currentContext != null) { var currentRouteData = currentContext.RouteData; - if (currentRouteData.DataTokens.ContainsKey(Core.Constants.Web.UmbracoRouteDefinitionDataToken)) - return Attempt.Succeed((RouteDefinition)currentRouteData.DataTokens[Core.Constants.Web.UmbracoRouteDefinitionDataToken]); + if (currentRouteData.Values.ContainsKey(Core.Constants.Web.UmbracoRouteDefinitionDataToken)) + { + return Attempt.Succeed((RouteDefinition)currentRouteData.Values[Core.Constants.Web.UmbracoRouteDefinitionDataToken]); + } currentContext = currentContext.IsChildAction ? currentContext.ParentActionViewContext diff --git a/src/Umbraco.Web/Mvc/UmbracoPageResult.cs b/src/Umbraco.Web/Mvc/UmbracoPageResult.cs index 30c990a981..580924b909 100644 --- a/src/Umbraco.Web/Mvc/UmbracoPageResult.cs +++ b/src/Umbraco.Web/Mvc/UmbracoPageResult.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Web.Mvc; using System.Web.Routing; @@ -26,7 +26,7 @@ namespace Umbraco.Web.Mvc ValidateRouteData(context.RouteData); - var routeDef = (RouteDefinition)context.RouteData.DataTokens[Umbraco.Core.Constants.Web.UmbracoRouteDefinitionDataToken]; + var routeDef = (RouteDefinition)context.RouteData.Values[Umbraco.Core.Constants.Web.UmbracoRouteDefinitionDataToken]; var factory = ControllerBuilder.Current.GetControllerFactory(); context.RouteData.Values["action"] = routeDef.ActionName; @@ -72,7 +72,7 @@ namespace Umbraco.Web.Mvc /// private static void ValidateRouteData(RouteData routeData) { - if (routeData.DataTokens.ContainsKey(Umbraco.Core.Constants.Web.UmbracoRouteDefinitionDataToken) == false) + if (routeData.Values.ContainsKey(Umbraco.Core.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"); diff --git a/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs b/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs index 18e1fb8a1a..43dc341655 100644 --- a/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs +++ b/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text; using System.Web; using System.Web.Mvc; @@ -20,9 +20,7 @@ using Current = Umbraco.Web.Composing.Current; namespace Umbraco.Web.Mvc { - /// - /// Represents the properties and methods that are needed in order to render an Umbraco view. - /// + // TODO: This has been ported to netcore, just needs testing public abstract class UmbracoViewPage : WebViewPage { private readonly GlobalSettings _globalSettings; @@ -50,11 +48,9 @@ namespace Umbraco.Web.Mvc // like the Services & ApplicationCache properties, and have a setter for those special weird // cases. - /// - /// Gets the Umbraco context. - /// - public IUmbracoContext UmbracoContext => _umbracoContext - ?? (_umbracoContext = ViewContext.GetUmbracoContext() ?? Current.UmbracoContext); + // TODO: Can be injected to the view in netcore, else injected to the base model + // public IUmbracoContext UmbracoContext => _umbracoContext + // ?? (_umbracoContext = ViewContext.GetUmbracoContext() ?? Current.UmbracoContext); /// /// Gets the public content request. @@ -63,21 +59,27 @@ namespace Umbraco.Web.Mvc { get { - const string token = Core.Constants.Web.PublishedDocumentRequestDataToken; + // TODO: we only have one data token for a route now: Constants.Web.UmbracoRouteDefinitionDataToken - // we should always try to return the object from the data tokens just in case its a custom object and not - // the one from UmbracoContext. Fallback to UmbracoContext if necessary. + throw new NotImplementedException("Probably needs to be ported to netcore"); - // try view context - if (ViewContext.RouteData.DataTokens.ContainsKey(token)) - return (IPublishedRequest) ViewContext.RouteData.DataTokens.GetRequiredObject(token); + //// we should always try to return the object from the data tokens just in case its a custom object and not + //// the one from UmbracoContext. Fallback to UmbracoContext if necessary. - // child action, try parent view context - if (ViewContext.IsChildAction && ViewContext.ParentActionViewContext.RouteData.DataTokens.ContainsKey(token)) - return (IPublishedRequest) ViewContext.ParentActionViewContext.RouteData.DataTokens.GetRequiredObject(token); + //// try view context + //if (ViewContext.RouteData.DataTokens.ContainsKey(Constants.Web.UmbracoRouteDefinitionDataToken)) + //{ + // return (IPublishedRequest) ViewContext.RouteData.DataTokens.GetRequiredObject(Constants.Web.UmbracoRouteDefinitionDataToken); + //} - // fallback to UmbracoContext - return UmbracoContext.PublishedRequest; + //// child action, try parent view context + //if (ViewContext.IsChildAction && ViewContext.ParentActionViewContext.RouteData.DataTokens.ContainsKey(Constants.Web.UmbracoRouteDefinitionDataToken)) + //{ + // return (IPublishedRequest) ViewContext.ParentActionViewContext.RouteData.DataTokens.GetRequiredObject(Constants.Web.UmbracoRouteDefinitionDataToken); + //} + + //// fallback to UmbracoContext + //return UmbracoContext.PublishedRequest; } } diff --git a/src/Umbraco.Web/Mvc/UmbracoVirtualNodeRouteHandler.cs b/src/Umbraco.Web/Mvc/UmbracoVirtualNodeRouteHandler.cs index c00eb24cca..dc922d9fd2 100644 --- a/src/Umbraco.Web/Mvc/UmbracoVirtualNodeRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/UmbracoVirtualNodeRouteHandler.cs @@ -1,4 +1,4 @@ -using System.Web; +using System.Web; using System.Web.Mvc; using System.Web.Routing; using Microsoft.Extensions.DependencyInjection; @@ -62,12 +62,12 @@ namespace Umbraco.Web.Mvc var renderModel = new ContentModel(umbracoContext.PublishedRequest.PublishedContent); // assigns the required tokens to the request - requestContext.RouteData.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, renderModel); - requestContext.RouteData.DataTokens.Add(Core.Constants.Web.PublishedDocumentRequestDataToken, umbracoContext.PublishedRequest); - requestContext.RouteData.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, umbracoContext); + //requestContext.RouteData.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, renderModel); + //requestContext.RouteData.DataTokens.Add(Core.Constants.Web.PublishedDocumentRequestDataToken, umbracoContext.PublishedRequest); + //requestContext.RouteData.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, umbracoContext); - // this is used just for a flag that this is an umbraco custom route - requestContext.RouteData.DataTokens.Add(Core.Constants.Web.CustomRouteDataToken, true); + //// this is used just for a flag that this is an umbraco custom route + //requestContext.RouteData.DataTokens.Add(Core.Constants.Web.CustomRouteDataToken, true); // Here we need to detect if a SurfaceController has posted var formInfo = RenderRouteHandler.GetFormInfo(requestContext); @@ -81,7 +81,7 @@ namespace Umbraco.Web.Mvc }; // set the special data token to the current route definition - requestContext.RouteData.DataTokens[Core.Constants.Web.UmbracoRouteDefinitionDataToken] = def; + requestContext.RouteData.Values[Core.Constants.Web.UmbracoRouteDefinitionDataToken] = def; return RenderRouteHandler.HandlePostedValues(requestContext, formInfo); } diff --git a/src/Umbraco.Web/Runtime/WebInitialComponent.cs b/src/Umbraco.Web/Runtime/WebInitialComponent.cs index 4fb1754946..c11d648b37 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComponent.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComponent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Web.Http; @@ -171,7 +171,9 @@ namespace Umbraco.Web.Runtime new[] { meta.ControllerNamespace }); if (route.DataTokens == null) // web api routes don't set the data tokens object route.DataTokens = new RouteValueDictionary(); - route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "api"); //ensure the umbraco token is set + + // TODO: Pretty sure this is not necessary, we'll see + //route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "api"); //ensure the umbraco token is set } private static void RouteLocalSurfaceController(Type controller, string umbracoPath) @@ -183,7 +185,10 @@ namespace Umbraco.Web.Runtime url, // URL to match new { controller = meta.ControllerName, action = "Index", id = UrlParameter.Optional }, new[] { meta.ControllerNamespace }); // look in this namespace to create the controller - route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "surface"); // ensure the umbraco token is set + + // TODO: Pretty sure this is not necessary, we'll see + //route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "surface"); // ensure the umbraco token is set + route.DataTokens.Add("UseNamespaceFallback", false); // don't look anywhere else except this namespace! // make it use our custom/special SurfaceMvcHandler route.RouteHandler = new SurfaceRouteHandler(); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 8867971657..758839314b 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -184,7 +184,6 @@ - From b5e3bc9e0d1406cf9af89713155e369bcadfa15e Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 11 Dec 2020 14:55:19 +1100 Subject: [PATCH 04/14] Fix the UmbracoViewPage and view model binding, combine the tests cases, remove IPublishedContentType2, front end is rendering --- src/Umbraco.Core/Models/IContentModel.cs | 22 ++- .../PublishedContent/IPublishedContentType.cs | 12 +- .../PublishedContent/PublishedContentType.cs | 4 +- .../PublishedContentTypeExtensions.cs | 24 --- .../BlockListPropertyValueConverter.cs | 8 +- .../PublishedContentTypeCache.cs | 8 +- .../ContentStore.cs | 5 +- .../BlockListPropertyValueConverterTests.cs | 10 +- .../ModelBinders/ContentModelBinderTests.cs | 179 ++++++++++++++--- .../ModelBinders/RenderModelBinderTests.cs | 182 ------------------ .../Views/UmbracoViewPageTests.cs | 62 +++--- .../AspNetCore/UmbracoViewPage.cs | 105 +++++++--- .../Extensions/RazorPageExtensions.cs | 19 +- .../ModelBinders/ContentModelBinder.cs | 54 ++++-- 14 files changed, 334 insertions(+), 360 deletions(-) delete mode 100644 src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeExtensions.cs delete mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/RenderModelBinderTests.cs diff --git a/src/Umbraco.Core/Models/IContentModel.cs b/src/Umbraco.Core/Models/IContentModel.cs index d0d4f175d7..692547aa3e 100644 --- a/src/Umbraco.Core/Models/IContentModel.cs +++ b/src/Umbraco.Core/Models/IContentModel.cs @@ -1,10 +1,30 @@ -using Umbraco.Core.Models; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web.Models { + /// + /// The basic view model returned for front-end Umbraco controllers + /// + /// + /// + /// exists in order to unify all view models in Umbraco, whether it's a normal template view or a partial view macro, or + /// a user's custom model that they have created when doing route hijacking or custom routes. + /// + /// + /// By default all front-end template views inherit from UmbracoViewPage which has a model of but the model returned + /// from the controllers is which in normal circumstances would not work. This works with UmbracoViewPage because it + /// performs model binding between IContentModel and IPublishedContent. This offers a lot of flexibility when rendering views. In some cases if you + /// are route hijacking and returning a custom implementation of and your view is strongly typed to this model, you can still + /// render partial views created in the back office that have the default model of IPublishedContent without having to worry about explicitly passing + /// that model to the view. + /// + /// public interface IContentModel { + /// + /// Gets the + /// IPublishedContent Content { get; } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs index cfc789324a..f9330176aa 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace Umbraco.Core.Models.PublishedContent @@ -8,21 +8,13 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Instances implementing the interface should be /// immutable, ie if the content type changes, then a new instance needs to be created. - public interface IPublishedContentType2 : IPublishedContentType + public interface IPublishedContentType { /// /// Gets the unique key for the content type. /// Guid Key { get; } - } - /// - /// Represents an type. - /// - /// Instances implementing the interface should be - /// immutable, ie if the content type changes, then a new instance needs to be created. - public interface IPublishedContentType - { /// /// Gets the content type identifier. /// diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index 14c26442eb..daf75f5c50 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -9,7 +9,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Instances of the class are immutable, ie /// if the content type changes, then a new class needs to be created. - public class PublishedContentType : IPublishedContentType2 + public class PublishedContentType : IPublishedContentType { private readonly IPublishedPropertyType[] _propertyTypes; diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeExtensions.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeExtensions.cs deleted file mode 100644 index feab33c1d6..0000000000 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace Umbraco.Core.Models.PublishedContent -{ - public static class PublishedContentTypeExtensions - { - /// - /// Get the GUID key from an - /// - /// - /// - /// - public static bool TryGetKey(this IPublishedContentType publishedContentType, out Guid key) - { - if (publishedContentType is IPublishedContentType2 contentTypeWithKey) - { - key = contentTypeWithKey.Key; - return true; - } - key = Guid.Empty; - return false; - } - } -} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index f46c118174..f35f9b9469 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; @@ -103,8 +103,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters if (settingGuidUdi != null) settingsPublishedElements.TryGetValue(settingGuidUdi.Guid, out settingsData); - if (!contentData.ContentType.TryGetKey(out var contentTypeKey)) - throw new InvalidOperationException("The content type was not of type " + typeof(IPublishedContentType2)); + var contentTypeKey = contentData.ContentType.Key; if (!blockConfigMap.TryGetValue(contentTypeKey, out var blockConfig)) continue; @@ -113,8 +112,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters // we also ensure that the content type's match since maybe the settings type has been changed after this has been persisted. if (settingsData != null) { - if (!settingsData.ContentType.TryGetKey(out var settingsElementTypeKey)) - throw new InvalidOperationException("The settings element type was not of type " + typeof(IPublishedContentType2)); + var settingsElementTypeKey = settingsData.ContentType.Key; if (!blockConfig.SettingsElementTypeKey.HasValue || settingsElementTypeKey != blockConfig.SettingsElementTypeKey) settingsData = null; diff --git a/src/Umbraco.Infrastructure/PublishedCache/PublishedContentTypeCache.cs b/src/Umbraco.Infrastructure/PublishedCache/PublishedContentTypeCache.cs index 4c1482c82c..ae99243a2c 100644 --- a/src/Umbraco.Infrastructure/PublishedCache/PublishedContentTypeCache.cs +++ b/src/Umbraco.Infrastructure/PublishedCache/PublishedContentTypeCache.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -190,8 +190,7 @@ namespace Umbraco.Web.PublishedCache try { _lock.EnterWriteLock(); - if (type.TryGetKey(out var key)) - _keyToIdMap[key] = type.Id; + _keyToIdMap[type.Key] = type.Id; return _typesByAlias[aliasKey] = _typesById[type.Id] = type; } finally @@ -227,8 +226,7 @@ namespace Umbraco.Web.PublishedCache try { _lock.EnterWriteLock(); - if (type.TryGetKey(out var key)) - _keyToIdMap[key] = type.Id; + _keyToIdMap[type.Key] = type.Id; return _typesByAlias[GetAliasKey(type)] = _typesById[type.Id] = type; } finally diff --git a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs b/src/Umbraco.PublishedCache.NuCache/ContentStore.cs index d41ca344dc..e79c195b46 100644 --- a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs +++ b/src/Umbraco.PublishedCache.NuCache/ContentStore.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -1167,8 +1167,7 @@ namespace Umbraco.Web.PublishedCache.NuCache SetValueLocked(_contentTypesById, type.Id, type); SetValueLocked(_contentTypesByAlias, type.Alias, type); // ensure the key/id map is accurate - if (type.TryGetKey(out var key)) - _contentTypeKeyToIdMap[key] = type.Id; + _contentTypeKeyToIdMap[type.Key] = type.Id; } // set a node (just the node, not the tree) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs index eb77ad2e1c..dc6d059a0a 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs @@ -1,4 +1,4 @@ -using Moq; +using Moq; using NUnit.Framework; using System; using System.Collections.Generic; @@ -33,19 +33,19 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.PropertyEditors /// private IPublishedSnapshotAccessor GetPublishedSnapshotAccessor() { - var test1ContentType = Mock.Of(x => + var test1ContentType = Mock.Of(x => x.IsElement == true && x.Key == ContentKey1 && x.Alias == ContentAlias1); - var test2ContentType = Mock.Of(x => + var test2ContentType = Mock.Of(x => x.IsElement == true && x.Key == ContentKey2 && x.Alias == ContentAlias2); - var test3ContentType = Mock.Of(x => + var test3ContentType = Mock.Of(x => x.IsElement == true && x.Key == SettingKey1 && x.Alias == SettingAlias1); - var test4ContentType = Mock.Of(x => + var test4ContentType = Mock.Of(x => x.IsElement == true && x.Key == SettingKey2 && x.Alias == SettingAlias2); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs index ba5910da29..caac2f9207 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -11,57 +12,95 @@ using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Common.ModelBinders; using Umbraco.Web.Common.Routing; using Umbraco.Web.Models; -using Umbraco.Web.Website.Routing; namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders { [TestFixture] public class ContentModelBinderTests { + private ContentModelBinder _contentModelBinder; + + [SetUp] + public void SetUp() => _contentModelBinder = new ContentModelBinder(); + [Test] - public void Does_Not_Bind_Model_When_UmbracoDataToken_Not_In_Route_Data() + [TestCase(typeof(IPublishedContent), false)] + [TestCase(typeof(ContentModel), false)] + [TestCase(typeof(ContentType1), false)] + [TestCase(typeof(ContentModel), false)] + [TestCase(typeof(NonContentModel), true)] + [TestCase(typeof(MyCustomContentModel), true)] + [TestCase(typeof(IContentModel), true)] + public void Returns_Binder_For_IPublishedContent_And_IRenderModel(Type testType, bool expectNull) + { + var binderProvider = new ContentModelBinderProvider(); + var contextMock = new Mock(); + contextMock.Setup(x => x.Metadata).Returns(new EmptyModelMetadataProvider().GetMetadataForType(testType)); + + IModelBinder found = binderProvider.GetBinder(contextMock.Object); + if (expectNull) + { + Assert.IsNull(found); + } + else + { + Assert.IsNotNull(found); + } + } + + [Test] + public async Task Does_Not_Bind_Model_When_UmbracoToken_Not_In_Route_Values() { // Arrange IPublishedContent pc = CreatePublishedContent(); - var bindingContext = CreateBindingContext(typeof(ContentModel), pc, withUmbracoDataToken: false); - var binder = new ContentModelBinder(); + var bindingContext = CreateBindingContextForUmbracoRequest(typeof(ContentModel), pc); + bindingContext.ActionContext.RouteData.Values.Remove(Constants.Web.UmbracoRouteDefinitionDataToken); // Act - binder.BindModelAsync(bindingContext); + await _contentModelBinder.BindModelAsync(bindingContext); // Assert Assert.False(bindingContext.Result.IsModelSet); } [Test] - public void Does_Not_Bind_Model_When_Source_Not_Of_Expected_Type() + public async Task Does_Not_Bind_Model_When_UmbracoToken_Has_Incorrect_Model() { // Arrange IPublishedContent pc = CreatePublishedContent(); - var bindingContext = CreateBindingContext(typeof(ContentModel), pc, source: new NonContentModel()); - var binder = new ContentModelBinder(); + var bindingContext = CreateBindingContextForUmbracoRequest(typeof(ContentModel), pc); + bindingContext.ActionContext.RouteData.Values[Constants.Web.UmbracoRouteDefinitionDataToken] = new NonContentModel(); // Act - binder.BindModelAsync(bindingContext); + await _contentModelBinder.BindModelAsync(bindingContext); // Assert Assert.False(bindingContext.Result.IsModelSet); } [Test] - public void BindModel_Returns_If_Same_Type() + public async Task Bind_Model_When_UmbracoToken_Is_In_Route_Values() { // Arrange IPublishedContent pc = CreatePublishedContent(); - var content = new ContentModel(pc); - var bindingContext = CreateBindingContext(typeof(ContentModel), pc, source: content); - var binder = new ContentModelBinder(); + var bindingContext = CreateBindingContextForUmbracoRequest(typeof(ContentModel), pc); // Act - binder.BindModelAsync(bindingContext); + await _contentModelBinder.BindModelAsync(bindingContext); // Assert - Assert.AreSame(content, bindingContext.Result.Model); + Assert.True(bindingContext.Result.IsModelSet); + } + + [Test] + public void Throws_When_Source_Not_Of_Expected_Type() + { + // Arrange + IPublishedContent pc = CreatePublishedContent(); + var bindingContext = new DefaultModelBindingContext(); + + // Act/Assert + Assert.Throws(() => _contentModelBinder.BindModel(bindingContext, new NonContentModel(), typeof(ContentModel))); } [Test] @@ -69,11 +108,10 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders { // Arrange IPublishedContent pc = CreatePublishedContent(); - var bindingContext = CreateBindingContext(typeof(ContentModel), pc, source: pc); - var binder = new ContentModelBinder(); + var bindingContext = new DefaultModelBindingContext(); // Act - binder.BindModelAsync(bindingContext); + _contentModelBinder.BindModel(bindingContext, pc, typeof(ContentModel)); // Assert Assert.True(bindingContext.Result.IsModelSet); @@ -84,24 +122,95 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders { // Arrange IPublishedContent pc = CreatePublishedContent(); - var bindingContext = CreateBindingContext(typeof(ContentModel), pc, source: new ContentModel(new ContentType2(pc))); - var binder = new ContentModelBinder(); + var bindingContext = new DefaultModelBindingContext(); // Act - binder.BindModelAsync(bindingContext); + _contentModelBinder.BindModel(bindingContext, new ContentModel(new ContentType2(pc)), typeof(ContentModel)); // Assert Assert.True(bindingContext.Result.IsModelSet); } - private ModelBindingContext CreateBindingContext(Type modelType, IPublishedContent publishedContent, bool withUmbracoDataToken = true, object source = null) + [Test] + public void BindModel_Null_Source_Returns_Null() + { + var bindingContext = new DefaultModelBindingContext(); + _contentModelBinder.BindModel(bindingContext, null, typeof(ContentType1)); + Assert.IsNull(bindingContext.Result.Model); + } + + [Test] + public void BindModel_Returns_If_Same_Type() + { + var content = new ContentType1(Mock.Of()); + var bindingContext = new DefaultModelBindingContext(); + + _contentModelBinder.BindModel(bindingContext, content, typeof(ContentType1)); + + Assert.AreSame(content, bindingContext.Result.Model); + } + + [Test] + public void BindModel_RenderModel_To_IPublishedContent() + { + var content = new ContentType1(Mock.Of()); + var renderModel = new ContentModel(content); + + var bindingContext = new DefaultModelBindingContext(); + _contentModelBinder.BindModel(bindingContext, renderModel, typeof(IPublishedContent)); + + Assert.AreSame(content, bindingContext.Result.Model); + } + + [Test] + public void BindModel_IPublishedContent_To_RenderModel() + { + var content = new ContentType1(Mock.Of()); + var bindingContext = new DefaultModelBindingContext(); + + _contentModelBinder.BindModel(bindingContext, content, typeof(ContentModel)); + var bound = (IContentModel)bindingContext.Result.Model; + + Assert.AreSame(content, bound.Content); + } + + [Test] + public void BindModel_IPublishedContent_To_Generic_RenderModel() + { + var content = new ContentType1(Mock.Of()); + var bindingContext = new DefaultModelBindingContext(); + + _contentModelBinder.BindModel(bindingContext, content, typeof(ContentModel)); + var bound = (IContentModel)bindingContext.Result.Model; + + Assert.AreSame(content, bound.Content); + } + + [Test] + public void Null_Model_Binds_To_Null() + { + IPublishedContent pc = Mock.Of(); + var bindingContext = new DefaultModelBindingContext(); + _contentModelBinder.BindModel(bindingContext, null, typeof(ContentModel)); + Assert.IsNull(bindingContext.Result.Model); + } + + [Test] + public void Invalid_Model_Type_Throws_Exception() + { + IPublishedContent pc = Mock.Of(); + var bindingContext = new DefaultModelBindingContext(); + Assert.Throws(() => _contentModelBinder.BindModel(bindingContext, "Hello", typeof(IPublishedContent))); + } + + /// + /// Creates a binding context with the route values populated to similute an Umbraco dynamically routed request + /// + private ModelBindingContext CreateBindingContextForUmbracoRequest(Type modelType, IPublishedContent publishedContent) { var httpContext = new DefaultHttpContext(); var routeData = new RouteData(); - if (withUmbracoDataToken) - { - routeData.Values.Add(Constants.Web.UmbracoRouteDefinitionDataToken, new UmbracoRouteValues(publishedContent)); - } + routeData.Values.Add(Constants.Web.UmbracoRouteDefinitionDataToken, new UmbracoRouteValues(publishedContent)); var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor()); var metadataProvider = new EmptyModelMetadataProvider(); @@ -120,19 +229,25 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders { } - private IPublishedContent CreatePublishedContent() - { - return new ContentType2(new Mock().Object); - } + private IPublishedContent CreatePublishedContent() => new ContentType2(new Mock().Object); public class ContentType1 : PublishedContentWrapped { - public ContentType1(IPublishedContent content) : base(content) { } + public ContentType1(IPublishedContent content) + : base(content) { } } public class ContentType2 : ContentType1 { - public ContentType2(IPublishedContent content) : base(content) { } + public ContentType2(IPublishedContent content) + : base(content) { } + } + + public class MyCustomContentModel : ContentModel + { + public MyCustomContentModel(IPublishedContent content) + : base(content) + { } } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/RenderModelBinderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/RenderModelBinderTests.cs deleted file mode 100644 index 660a9b7bd1..0000000000 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/RenderModelBinderTests.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Routing; -using Moq; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web.Common.ModelBinders; -using Umbraco.Web.Common.Routing; -using Umbraco.Web.Models; -using Umbraco.Web.Website.Routing; - -namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders -{ - [TestFixture] - public class RenderModelBinderTests - { - private ContentModelBinder _contentModelBinder; - [SetUp] - public void SetUp() - { - _contentModelBinder = new ContentModelBinder(); - } - - [Test] - [TestCase(typeof(IPublishedContent), false)] - [TestCase(typeof(ContentModel), false)] - [TestCase(typeof(MyContent), false)] - [TestCase(typeof(ContentModel), false)] - [TestCase(typeof(MyOtherContent), true)] - [TestCase(typeof(MyCustomContentModel), true)] - [TestCase(typeof(IContentModel), true)] - public void Returns_Binder_For_IPublishedContent_And_IRenderModel(Type testType, bool expectNull) - { - var binderProvider = new ContentModelBinderProvider(); - var contextMock = new Mock(); - contextMock.Setup(x => x.Metadata).Returns(new EmptyModelMetadataProvider().GetMetadataForType(testType)); - - var found = binderProvider.GetBinder(contextMock.Object); - if (expectNull) - { - Assert.IsNull(found); - } - else - { - Assert.IsNotNull(found); - } - } - - [Test] - public void BindModel_Null_Source_Returns_Null() - { - var bindingContext = new DefaultModelBindingContext(); - _contentModelBinder.BindModelAsync(bindingContext, null, typeof(MyContent)); - Assert.IsNull(bindingContext.Result.Model); - } - - [Test] - public void BindModel_Returns_If_Same_Type() - { - var content = new MyContent(Mock.Of()); - var bindingContext = new DefaultModelBindingContext(); - - _contentModelBinder.BindModelAsync(bindingContext, content, typeof(MyContent)); - - Assert.AreSame(content, bindingContext.Result.Model); - } - - [Test] - public void BindModel_RenderModel_To_IPublishedContent() - { - var content = new MyContent(Mock.Of()); - var renderModel = new ContentModel(content); - - var bindingContext = new DefaultModelBindingContext(); - _contentModelBinder.BindModelAsync(bindingContext, renderModel, typeof(IPublishedContent)); - - Assert.AreSame(content, bindingContext.Result.Model); - } - - [Test] - public void BindModel_IPublishedContent_To_RenderModel() - { - var content = new MyContent(Mock.Of()); - var bindingContext = new DefaultModelBindingContext(); - - _contentModelBinder.BindModelAsync(bindingContext, content, typeof(ContentModel)); - var bound = (IContentModel) bindingContext.Result.Model; - - Assert.AreSame(content, bound.Content); - } - - [Test] - public void BindModel_IPublishedContent_To_Generic_RenderModel() - { - var content = new MyContent(Mock.Of()); - var bindingContext = new DefaultModelBindingContext(); - - _contentModelBinder.BindModelAsync(bindingContext, content, typeof(ContentModel)); - var bound = (IContentModel) bindingContext.Result.Model; - - Assert.AreSame(content, bound.Content); - } - - [Test] - public void No_DataToken_Returns_Null() - { - IPublishedContent pc = Mock.Of(); - var content = new MyContent(pc); - var bindingContext = CreateBindingContext(typeof(ContentModel), pc, false, content); - - _contentModelBinder.BindModelAsync(bindingContext); - - Assert.IsNull(bindingContext.Result.Model); - } - - [Test] - public void Invalid_DataToken_Model_Type_Returns_Null() - { - IPublishedContent pc = Mock.Of(); - var bindingContext = CreateBindingContext(typeof(IPublishedContent), pc, source: "Hello"); - _contentModelBinder.BindModelAsync(bindingContext); - Assert.IsNull(bindingContext.Result.Model); - } - - [Test] - public void IPublishedContent_DataToken_Model_Type_Uses_DefaultImplementation() - { - IPublishedContent pc = Mock.Of(); - var content = new MyContent(pc); - var bindingContext = CreateBindingContext(typeof(MyContent), pc, source: content); - - _contentModelBinder.BindModelAsync(bindingContext); - - Assert.AreEqual(content, bindingContext.Result.Model); - } - - private ModelBindingContext CreateBindingContext(Type modelType, IPublishedContent publishedContent, bool withUmbracoDataToken = true, object source = null) - { - var httpContext = new DefaultHttpContext(); - var routeData = new RouteData(); - if (withUmbracoDataToken) - { - routeData.Values.Add(Constants.Web.UmbracoRouteDefinitionDataToken, new UmbracoRouteValues(publishedContent)); - } - - var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor()); - var metadataProvider = new EmptyModelMetadataProvider(); - var routeValueDictionary = new RouteValueDictionary(); - var valueProvider = new RouteValueProvider(BindingSource.Path, routeValueDictionary); - return new DefaultModelBindingContext - { - ActionContext = actionContext, - ModelMetadata = metadataProvider.GetMetadataForType(modelType), - ModelName = modelType.Name, - ValueProvider = valueProvider, - }; - } - - public class MyCustomContentModel : ContentModel - { - public MyCustomContentModel(IPublishedContent content) - : base(content) - { } - } - - public class MyOtherContent - { - - } - - public class MyContent : PublishedContentWrapped - { - public MyContent(IPublishedContent content) : base(content) - { - } - } - } -} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Views/UmbracoViewPageTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Views/UmbracoViewPageTests.cs index 3b52d0701e..03065d4bcb 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Views/UmbracoViewPageTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Views/UmbracoViewPageTests.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using NUnit.Framework; using Umbraco.Core.Models.PublishedContent; @@ -13,7 +14,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Views [TestFixture] public class UmbracoViewPageTests { - #region RenderModel To ... [Test] public void RenderModel_To_RenderModel() { @@ -58,7 +58,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Views var view = new ContentType2TestPage(); var viewData = GetViewDataDictionary(model); - Assert.ThrowsAsync(async () => await view.SetViewDataAsyncX(viewData)); + Assert.Throws(() => view.SetViewData(viewData)); } [Test] @@ -96,12 +96,9 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Views var view = new RenderModelOfContentType2TestPage(); var viewData = GetViewDataDictionary(model); - Assert.ThrowsAsync(async () => await view.SetViewDataAsyncX(viewData)); + Assert.Throws(() => view.SetViewData(viewData)); } - #endregion - - #region RenderModelOf To ... [Test] public void RenderModelOf_ContentType1_To_RenderModel() @@ -117,20 +114,20 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Views } [Test] - public async Task RenderModelOf_ContentType1_To_ContentType1() + public void RenderModelOf_ContentType1_To_ContentType1() { var content = new ContentType1(null); var model = new ContentModel(content); var view = new ContentType1TestPage(); var viewData = GetViewDataDictionary>(model); - await view.SetViewDataAsyncX(viewData); + view.SetViewData(viewData); Assert.IsInstanceOf(view.Model); } [Test] - public async Task RenderModelOf_ContentType2_To_ContentType1() + public void RenderModelOf_ContentType2_To_ContentType1() { var content = new ContentType2(null); var model = new ContentModel(content); @@ -140,13 +137,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Views Model = model }; - await view.SetViewDataAsyncX(viewData); + view.SetViewData(viewData); Assert.IsInstanceOf(view.Model); } [Test] - public async Task RenderModelOf_ContentType1_To_ContentType2() + public void RenderModelOf_ContentType1_To_ContentType2() { var content = new ContentType1(null); @@ -154,7 +151,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Views var view = new ContentType2TestPage(); var viewData = GetViewDataDictionary(model); - Assert.ThrowsAsync(async () => await view.SetViewDataAsyncX(viewData)); + Assert.Throws(() => view.SetViewData(viewData)); } [Test] @@ -172,14 +169,14 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Views } [Test] - public async Task RenderModelOf_ContentType2_To_RenderModelOf_ContentType1() + public void RenderModelOf_ContentType2_To_RenderModelOf_ContentType1() { var content = new ContentType2(null); var model = new ContentModel(content); var view = new RenderModelOfContentType1TestPage(); var viewData = GetViewDataDictionary>(model); - await view.SetViewDataAsyncX(viewData); + view.SetViewData(viewData); Assert.IsInstanceOf>(view.Model); Assert.IsInstanceOf(view.Model.Content); @@ -193,48 +190,44 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Views var view = new RenderModelOfContentType2TestPage(); var viewData = GetViewDataDictionary(model); - Assert.ThrowsAsync(async () => await view.SetViewDataAsyncX(viewData)); + Assert.Throws(() => view.SetViewData(viewData)); } - #endregion - - #region ContentType To ... - [Test] - public async Task ContentType1_To_RenderModel() + public void ContentType1_To_RenderModel() { var content = new ContentType1(null); var view = new RenderModelTestPage(); var viewData = GetViewDataDictionary(content); - await view.SetViewDataAsyncX(viewData); + view.SetViewData(viewData); Assert.IsInstanceOf(view.Model); } [Test] - public async Task ContentType1_To_RenderModelOf_ContentType1() + public void ContentType1_To_RenderModelOf_ContentType1() { var content = new ContentType1(null); var view = new RenderModelOfContentType1TestPage(); var viewData = GetViewDataDictionary(content); - await view.SetViewDataAsyncX(viewData); + view.SetViewData(viewData); Assert.IsInstanceOf>(view.Model); Assert.IsInstanceOf(view.Model.Content); } [Test] - public async Task ContentType2_To_RenderModelOf_ContentType1() + public void ContentType2_To_RenderModelOf_ContentType1() { // Same as above but with ContentModel var content = new ContentType2(null); var view = new RenderModelOfContentType1TestPage(); var viewData = GetViewDataDictionary(content); - await view.SetViewDataAsyncX(viewData); + view.SetViewData(viewData); Assert.IsInstanceOf>(view.Model); Assert.IsInstanceOf(view.Model.Content); @@ -247,17 +240,17 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Views var view = new RenderModelOfContentType2TestPage(); var viewData = GetViewDataDictionary(content); - Assert.ThrowsAsync(async () => await view.SetViewDataAsyncX(viewData)); + Assert.Throws(() => view.SetViewData(viewData)); } [Test] - public async Task ContentType1_To_ContentType1() + public void ContentType1_To_ContentType1() { var content = new ContentType1(null); var view = new ContentType1TestPage(); var viewData = GetViewDataDictionary(content); - await view.SetViewDataAsyncX(viewData); + view.SetViewData(viewData); Assert.IsInstanceOf(view.Model); } @@ -269,23 +262,21 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Views var view = new ContentType2TestPage(); var viewData = GetViewDataDictionary(content); - Assert.ThrowsAsync(async () => await view.SetViewDataAsyncX(viewData)); + Assert.Throws(() => view.SetViewData(viewData)); } [Test] - public async Task ContentType2_To_ContentType1() + public void ContentType2_To_ContentType1() { var content = new ContentType2(null); var view = new ContentType1TestPage(); var viewData = GetViewDataDictionary(content); - await view.SetViewDataAsyncX(viewData); + view.SetViewData(viewData); Assert.IsInstanceOf(view.Model); } - #endregion - #region Test helpers methods private ViewDataDictionary GetViewDataDictionary(object model) @@ -324,10 +315,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Views throw new NotImplementedException(); } - public async Task SetViewDataAsyncX(ViewDataDictionary viewData) - { - await SetViewDataAsync(viewData); - } + public void SetViewData(ViewDataDictionary viewData) => ViewData = (ViewDataDictionary)BindViewData(viewData); } public class RenderModelTestPage : TestPage diff --git a/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs b/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs index a97b67a900..3afc8978b6 100644 --- a/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs +++ b/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -16,6 +17,7 @@ using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Strings; using Umbraco.Extensions; using Umbraco.Web.Common.ModelBinders; +using Umbraco.Web.Models; namespace Umbraco.Web.Common.AspNetCore { @@ -28,23 +30,44 @@ namespace Umbraco.Web.Common.AspNetCore public abstract class UmbracoViewPage : RazorPage { - private IUmbracoContext _umbracoContext; + private IUmbracoContextAccessor UmbracoContextAccessor => Context.RequestServices.GetRequiredService(); + private GlobalSettings GlobalSettings => Context.RequestServices.GetRequiredService>().Value; + private ContentSettings ContentSettings => Context.RequestServices.GetRequiredService>().Value; + private IProfilerHtml ProfilerHtml => Context.RequestServices.GetRequiredService(); + private IIOHelper IOHelper => Context.RequestServices.GetRequiredService(); + private ContentModelBinder ContentModelBinder => new ContentModelBinder(); + /// + /// Gets the + /// protected IUmbracoContext UmbracoContext => _umbracoContext ??= UmbracoContextAccessor.UmbracoContext; + /// + public override ViewContext ViewContext + { + get => base.ViewContext; + set + { + // Here we do the magic model swap + ViewContext ctx = value; + ctx.ViewData = BindViewData(ctx.ViewData); + base.ViewContext = ctx; + } + } + /// public override void Write(object value) { if (value is IHtmlEncodedString htmlEncodedString) { - base.WriteLiteral(htmlEncodedString.ToHtmlString()); + WriteLiteral(htmlEncodedString.ToHtmlString()); } else { @@ -52,10 +75,12 @@ namespace Umbraco.Web.Common.AspNetCore } } + /// public override void WriteLiteral(object value) { // filter / add preview banner - if (Context.Response.ContentType.InvariantEquals("text/html")) // ASP.NET default value + // ASP.NET default value is text/html + if (Context.Response.ContentType.InvariantEquals("text/html")) { if (UmbracoContext.IsDebug || UmbracoContext.InPreviewMode) { @@ -70,7 +95,8 @@ namespace Umbraco.Web.Common.AspNetCore { // creating previewBadge markup markupToInject = - string.Format(ContentSettings.PreviewBadge, + string.Format( + ContentSettings.PreviewBadge, IOHelper.ResolveUrl(GlobalSettings.UmbracoPath), Context.Request.GetEncodedUrl(), UmbracoContext.PublishedRequest.PublishedContent.Id); @@ -84,7 +110,7 @@ namespace Umbraco.Web.Common.AspNetCore var sb = new StringBuilder(text); sb.Insert(pos, markupToInject); - base.WriteLiteral(sb.ToString()); + WriteLiteral(sb.ToString()); return; } } @@ -93,70 +119,93 @@ namespace Umbraco.Web.Common.AspNetCore base.WriteLiteral(value); } - // TODO: This trick doesn't work anymore, this method used to be an override. - // Now the model is bound in a different place - // maps model - protected async Task SetViewDataAsync(ViewDataDictionary viewData) + /// + /// Dynamically binds the incoming to the required + /// + /// + /// This is used in order to provide the ability for an Umbraco view to either have a model of type + /// or . This will use the to bind the models + /// to the correct output type. + /// + protected ViewDataDictionary BindViewData(ViewDataDictionary viewData) { + // check if it's already the correct type and continue if it is + if (viewData is ViewDataDictionary vdd) + { + return vdd; + } + + // Here we hand the default case where we know the incoming model is ContentModel and the + // outgoing model is IPublishedContent. This is a fast conversion that doesn't require doing the full + // model binding, allocating classes, etc... + if (viewData.ModelMetadata.ModelType == typeof(ContentModel) + && typeof(TModel) == typeof(IPublishedContent)) + { + var contentModel = (ContentModel)viewData.Model; + viewData.Model = contentModel.Content; + return viewData; + } + // capture the model before we tinker with the viewData var viewDataModel = viewData.Model; // map the view data (may change its type, may set model to null) - viewData = MapViewDataDictionary(viewData, typeof (TModel)); + viewData = MapViewDataDictionary(viewData, typeof(TModel)); // bind the model var bindingContext = new DefaultModelBindingContext(); - await ContentModelBinder.BindModelAsync(bindingContext, viewDataModel, typeof (TModel)); + ContentModelBinder.BindModel(bindingContext, viewDataModel, typeof(TModel)); viewData.Model = bindingContext.Result.Model; - // set the view data - ViewData = (ViewDataDictionary) viewData; + // return the new view data + return (ViewDataDictionary)viewData; } // viewData is the ViewDataDictionary (maybe ) that we have // modelType is the type of the model that we need to bind to - // // figure out whether viewData can accept modelType else replace it - // private static ViewDataDictionary MapViewDataDictionary(ViewDataDictionary viewData, Type modelType) { - var viewDataType = viewData.GetType(); - + Type viewDataType = viewData.GetType(); if (viewDataType.IsGenericType) { // ensure it is the proper generic type - var def = viewDataType.GetGenericTypeDefinition(); + Type def = viewDataType.GetGenericTypeDefinition(); if (def != typeof(ViewDataDictionary<>)) + { throw new Exception("Could not map viewData of type \"" + viewDataType.FullName + "\"."); + } // get the viewData model type and compare with the actual view model type: // viewData is ViewDataDictionary and we will want to assign an // object of type modelType to the Model property of type viewDataModelType, we // need to check whether that is possible - var viewDataModelType = viewDataType.GenericTypeArguments[0]; + Type viewDataModelType = viewDataType.GenericTypeArguments[0]; if (viewDataModelType.IsAssignableFrom(modelType)) + { return viewData; + } } // if not possible or it is not generic then we need to create a new ViewDataDictionary - var nViewDataType = typeof(ViewDataDictionary<>).MakeGenericType(modelType); + Type nViewDataType = typeof(ViewDataDictionary<>).MakeGenericType(modelType); var tViewData = new ViewDataDictionary(viewData) { Model = null }; // temp view data to copy values var nViewData = (ViewDataDictionary)Activator.CreateInstance(nViewDataType, tViewData); return nViewData; } - public HtmlString RenderSection(string name, HtmlString defaultContents) - { - return RazorPageExtensions.RenderSection(this, name, defaultContents); - } + /// + /// Renders a section with default content if the section isn't defined + /// + public HtmlString RenderSection(string name, HtmlString defaultContents) => RazorPageExtensions.RenderSection(this, name, defaultContents); - public HtmlString RenderSection(string name, string defaultContents) - { - return RazorPageExtensions.RenderSection(this, name, defaultContents); - } + /// + /// Renders a section with default content if the section isn't defined + /// + public HtmlString RenderSection(string name, string defaultContents) => RazorPageExtensions.RenderSection(this, name, defaultContents); } } diff --git a/src/Umbraco.Web.Common/Extensions/RazorPageExtensions.cs b/src/Umbraco.Web.Common/Extensions/RazorPageExtensions.cs index 884e2bbdbc..d6c3fb5715 100644 --- a/src/Umbraco.Web.Common/Extensions/RazorPageExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/RazorPageExtensions.cs @@ -1,19 +1,24 @@ -using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Razor; namespace Umbraco.Extensions { + /// + /// Extension methods for + /// public static class RazorPageExtensions { + /// + /// Renders a section with default content if the section isn't defined + /// public static HtmlString RenderSection(this RazorPage webPage, string name, HtmlString defaultContents) - { - return webPage.IsSectionDefined(name) ? webPage.RenderSection(name) : defaultContents; - } + => webPage.IsSectionDefined(name) ? webPage.RenderSection(name) : defaultContents; + /// + /// Renders a section with default content if the section isn't defined + /// public static HtmlString RenderSection(this RazorPage webPage, string name, string defaultContents) - { - return webPage.IsSectionDefined(name) ? webPage.RenderSection(name) : new HtmlString(defaultContents); - } + => webPage.IsSectionDefined(name) ? webPage.RenderSection(name) : new HtmlString(defaultContents); } } diff --git a/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs b/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs index d8178033c9..d747a4ff86 100644 --- a/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs +++ b/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -10,21 +10,24 @@ using Umbraco.Web.Models; namespace Umbraco.Web.Common.ModelBinders { /// - /// Maps view models, supporting mapping to and from any IPublishedContent or IContentModel. + /// Maps view models, supporting mapping to and from any or . /// public class ContentModelBinder : IModelBinder { + /// public Task BindModelAsync(ModelBindingContext bindingContext) { // Although this model binder is built to work both ways between IPublishedContent and IContentModel in reality - // only IPublishedContent will ever exist in the request. + // only IPublishedContent will ever exist in the request so when this model binder is used as an IModelBinder + // in the aspnet pipeline it will really only support converting from IPublishedContent which is contained + // in the UmbracoRouteValues --> IContentModel if (!bindingContext.ActionContext.RouteData.Values.TryGetValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken, out var source) || !(source is UmbracoRouteValues umbracoRouteValues)) { return Task.CompletedTask; } - BindModelAsync(bindingContext, umbracoRouteValues.PublishedContent, bindingContext.ModelType); + BindModel(bindingContext, umbracoRouteValues.PublishedContent, bindingContext.ModelType); return Task.CompletedTask; } @@ -35,34 +38,42 @@ namespace Umbraco.Web.Common.ModelBinders // { ContentModel, ContentModel, IPublishedContent } // to // { ContentModel, ContentModel, IPublishedContent } - // - public Task BindModelAsync(ModelBindingContext bindingContext, object source, Type modelType) + + /// + /// Attempts to bind the model + /// + public void BindModel(ModelBindingContext bindingContext, object source, Type modelType) { // Null model, return if (source == null) { - return Task.CompletedTask; + return; } // If types already match, return - var sourceType = source.GetType(); - if (sourceType.Inherits(modelType)) // includes == + Type sourceType = source.GetType(); + if (sourceType.Inherits(modelType)) { bindingContext.Result = ModelBindingResult.Success(source); - return Task.CompletedTask; + return; } // Try to grab the content var sourceContent = source as IPublishedContent; // check if what we have is an IPublishedContent if (sourceContent == null && sourceType.Implements()) + { // else check if it's an IContentModel, and get the content sourceContent = ((IContentModel)source).Content; + } + if (sourceContent == null) { // else check if we can convert it to a content - var attempt1 = source.TryConvertTo(); + Attempt attempt1 = source.TryConvertTo(); if (attempt1.Success) + { sourceContent = attempt1.Result; + } } // If we have a content @@ -77,41 +88,41 @@ namespace Umbraco.Web.Common.ModelBinders } bindingContext.Result = ModelBindingResult.Success(sourceContent); - return Task.CompletedTask; + return; } // If model is ContentModel, create and return if (modelType == typeof(ContentModel)) { bindingContext.Result = ModelBindingResult.Success(new ContentModel(sourceContent)); - return Task.CompletedTask; + return; } // If model is ContentModel, check content type, then create and return if (modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(ContentModel<>)) { - var targetContentType = modelType.GetGenericArguments()[0]; + Type targetContentType = modelType.GetGenericArguments()[0]; if (sourceContent.GetType().Inherits(targetContentType) == false) { ThrowModelBindingException(true, true, sourceContent.GetType(), targetContentType); } bindingContext.Result = ModelBindingResult.Success(Activator.CreateInstance(modelType, sourceContent)); - return Task.CompletedTask; + return; } } // Last chance : try to convert - var attempt2 = source.TryConvertTo(modelType); + Attempt attempt2 = source.TryConvertTo(modelType); if (attempt2.Success) { bindingContext.Result = ModelBindingResult.Success(attempt2.Result); - return Task.CompletedTask; + return; } // Fail ThrowModelBindingException(false, false, sourceType, modelType); - return Task.CompletedTask; + return; } private void ThrowModelBindingException(bool sourceContent, bool modelContent, Type sourceType, Type modelType) @@ -121,12 +132,18 @@ namespace Umbraco.Web.Common.ModelBinders // prepare message msg.Append("Cannot bind source"); if (sourceContent) + { msg.Append(" content"); + } + msg.Append(" type "); msg.Append(sourceType.FullName); msg.Append(" to model"); if (modelContent) + { msg.Append(" content"); + } + msg.Append(" type "); msg.Append(modelType.FullName); msg.Append("."); @@ -134,7 +151,6 @@ namespace Umbraco.Web.Common.ModelBinders // raise event, to give model factories a chance at reporting // the error with more details, and optionally request that // the application restarts. - var args = new ModelBindingArgs(sourceType, modelType, msg); ModelBindingException?.Invoke(this, args); From 776df77dfe22a95e2f3c628405b551555d583ede Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 11 Dec 2020 15:29:27 +1100 Subject: [PATCH 05/14] Notes and cleanup --- src/Umbraco.Core/Constants-Web.cs | 5 ----- src/Umbraco.Core/Web/IUmbracoContext.cs | 3 ++- .../Repositories/Implement/ContentRepositoryBase.cs | 9 ++++----- .../Implement/ContentTypeServiceBaseOfTItemTService.cs | 2 +- .../Services/EntityServiceTests.cs | 4 ---- .../Extensions/ApplicationBuilderExtensions.cs | 2 ++ .../Routing/UmbracoRouteValueTransformer.cs | 1 + 7 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index e29d793909..8199d9fbd0 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -7,11 +7,6 @@ namespace Umbraco.Core /// public static class Web { - // TODO: Need to review these... - //public const string UmbracoContextDataToken = "umbraco-context"; - //public const string UmbracoDataToken = "umbraco"; - //public const string PublishedDocumentRequestDataToken = "umbraco-doc-request"; - //public const string CustomRouteDataToken = "umbraco-custom-route"; public const string UmbracoRouteDefinitionDataToken = "umbraco-route-def"; /// diff --git a/src/Umbraco.Core/Web/IUmbracoContext.cs b/src/Umbraco.Core/Web/IUmbracoContext.cs index 681dedbfd2..7fa02e3b73 100644 --- a/src/Umbraco.Core/Web/IUmbracoContext.cs +++ b/src/Umbraco.Core/Web/IUmbracoContext.cs @@ -49,11 +49,12 @@ namespace Umbraco.Web /// /// Boolean value indicating whether the current request is a front-end umbraco request /// - bool IsFrontEndUmbracoRequest { get; } + bool IsFrontEndUmbracoRequest { get; } // TODO: This could easily be an ext method and mocking just means setting the published request to null /// /// Gets/sets the PublishedRequest object /// + // TODO: Can we refactor this and not expose this mutable object here? Instead just expose IPublishedContent? A bunch of stuff would need to change but would be better IPublishedRequest PublishedRequest { get; set; } /// diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 7ce363e446..a84b34b75a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -772,11 +772,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement * level now since we can fire these events within the transaction... * The reason these events 'need' to fire in the transaction is to ensure data consistency with Nucache (currently * the only thing that uses them). For example, if the transaction succeeds and NuCache listened to ContentService.Saved - * and then NuCache failed at persisting data after the trans completed, then NuCache would be out of sync. This way - * the entire trans is rolled back if NuCache files. That said, I'm unsure this is really required because there - * are other systems that rely on the "ed" (i.e. Saved) events like Examine which would be inconsistent if it failed - * too. I'm just not sure this is totally necessary especially. - * So these events can be moved to the service level. However, see the notes below, it seems the only event we + * and then NuCache failed at persisting data after the trans completed, then NuCache would be out of sync. This way + * the entire trans is rolled back if NuCache files. This is part of the discussion about removing the static events, + * possibly there's 3 levels of eventing, "ing", "scoped" (in trans) and "ed" (after trans). + * These particular events can be moved to the service level. However, see the notes below, it seems the only event we * really need is the ScopedEntityRefresh. The only tricky part with moving that to the service level is that the * handlers of that event will need to deal with the data a little differently because it seems that the * "Published" flag on the content item matters and this event is raised before that flag is switched. Weird. diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs index 1bdd00f576..be541486ff 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTItemTService.cs @@ -31,7 +31,7 @@ namespace Umbraco.Core.Services.Implement /// The purpose of this event being raised within the transaction is so that listeners can perform database /// operations from within the same transaction and guarantee data consistency so that if anything goes wrong /// the entire transaction can be rolled back. This is used by Nucache. - /// TODO: See remarks in ContentRepositoryBase about these types of events. Not sure we need/want them. + /// TODO: See remarks in ContentRepositoryBase about these types of events. /// public static event TypedEventHandler.EventArgs> ScopedRefreshedEntity; diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs index 4f4a852902..6d5cf19b50 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs @@ -42,10 +42,6 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services [SetUp] public void SetupTestData() { - // This is super nasty, but this lets us initialize the cache while it is empty. - // var publishedSnapshotService = GetRequiredService() as PublishedSnapshotService; - // publishedSnapshotService?.OnApplicationInit(null, EventArgs.Empty); - if (_langFr == null && _langEs == null) { var globalSettings = new GlobalSettings(); diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 7290aa9b0e..6fdd5c9be7 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -183,6 +183,8 @@ namespace Umbraco.Extensions /// public static IApplicationBuilder UseUmbracoContentCache(this IApplicationBuilder app) { + // TODO: This should install middleware to initialize instead of eagerly doing the initialize here + PublishedSnapshotServiceEventHandler publishedContentEvents = app.ApplicationServices.GetRequiredService(); publishedContentEvents.Start(); return app; diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index 2c1debc3ee..be7c9f7409 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -32,6 +32,7 @@ namespace Umbraco.Web.Website.Routing /// It seems as though with the "State" parameter we could more easily assign the IPublishedRequest or IPublishedContent /// or UmbracoContext more easily that way. In the meantime we will rely on assigning the IPublishedRequest to the /// route values along with the IPublishedContent to the umbraco context + /// have created a GH discussion here https://github.com/dotnet/aspnetcore/discussions/28562 we'll see if anyone responds /// public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer { From fc16669a91017efb5c991d8528395e1aa88981ed Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 14 Dec 2020 16:55:45 +1100 Subject: [PATCH 06/14] Moves cache initialization the the request middleware --- .../ApplicationBuilderExtensions.cs | 13 ------- .../Middleware/UmbracoRequestMiddleware.cs | 39 +++++++++++++++---- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 6fdd5c9be7..655867ebeb 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -37,7 +37,6 @@ namespace Umbraco.Extensions // We need to add this before UseRouting so that the UmbracoContext and other middlewares are executed // before endpoint routing middleware. app.UseUmbracoRouting(); - app.UseUmbracoContentCache(); app.UseStatusCodePages(); @@ -178,18 +177,6 @@ namespace Umbraco.Extensions return app; } - /// - /// Enables the Umbraco content cache - /// - public static IApplicationBuilder UseUmbracoContentCache(this IApplicationBuilder app) - { - // TODO: This should install middleware to initialize instead of eagerly doing the initialize here - - PublishedSnapshotServiceEventHandler publishedContentEvents = app.ApplicationServices.GetRequiredService(); - publishedContentEvents.Start(); - return app; - } - /// /// Ensures the runtime is shutdown when the application is shutting down /// diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs index 56f093ed2b..7845962928 100644 --- a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; @@ -8,6 +9,7 @@ using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Web.Common.Lifetime; +using Umbraco.Web.PublishedCache.NuCache; namespace Umbraco.Web.Common.Middleware { @@ -16,7 +18,12 @@ namespace Umbraco.Web.Common.Middleware /// Manages Umbraco request objects and their lifetime /// /// + /// + /// This is responsible for initializing the content cache + /// + /// /// This is responsible for creating and assigning an + /// /// public class UmbracoRequestMiddleware : IMiddleware { @@ -25,6 +32,10 @@ namespace Umbraco.Web.Common.Middleware private readonly IUmbracoContextFactory _umbracoContextFactory; private readonly IRequestCache _requestCache; private readonly IBackOfficeSecurityFactory _backofficeSecurityFactory; + private readonly PublishedSnapshotServiceEventHandler _publishedSnapshotServiceEventHandler; + private static bool s_cacheInitialized = false; + private static bool s_cacheInitializedFlag = false; + private static object s_cacheInitializedLock = new object(); /// /// Initializes a new instance of the class. @@ -34,13 +45,15 @@ namespace Umbraco.Web.Common.Middleware IUmbracoRequestLifetimeManager umbracoRequestLifetimeManager, IUmbracoContextFactory umbracoContextFactory, IRequestCache requestCache, - IBackOfficeSecurityFactory backofficeSecurityFactory) + IBackOfficeSecurityFactory backofficeSecurityFactory, + PublishedSnapshotServiceEventHandler publishedSnapshotServiceEventHandler) { _logger = logger; _umbracoRequestLifetimeManager = umbracoRequestLifetimeManager; _umbracoContextFactory = umbracoContextFactory; _requestCache = requestCache; _backofficeSecurityFactory = backofficeSecurityFactory; + _publishedSnapshotServiceEventHandler = publishedSnapshotServiceEventHandler; } /// @@ -55,6 +68,8 @@ namespace Umbraco.Web.Common.Middleware return; } + EnsureContentCacheInitialized(); + _backofficeSecurityFactory.EnsureBackOfficeSecurity(); // Needs to be before UmbracoContext, TODO: Why? UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); @@ -109,16 +124,15 @@ namespace Umbraco.Web.Common.Middleware /// /// Any object that is in the HttpContext.Items collection that is IDisposable will get disposed on the end of the request /// - /// - /// - /// private static void DisposeRequestCacheItems(ILogger logger, IRequestCache requestCache, Uri requestUri) { // do not process if client-side request if (requestUri.IsClientSideRequest()) + { return; + } - //get a list of keys to dispose + // get a list of keys to dispose var keys = new HashSet(); foreach (var i in requestCache) { @@ -127,7 +141,7 @@ namespace Umbraco.Web.Common.Middleware keys.Add(i.Key); } } - //dispose each item and key that was found as disposable. + // dispose each item and key that was found as disposable. foreach (var k in keys) { try @@ -149,6 +163,17 @@ namespace Umbraco.Web.Common.Middleware } } - + /// + /// Initializes the content cache one time + /// + private void EnsureContentCacheInitialized() => LazyInitializer.EnsureInitialized( + ref s_cacheInitialized, + ref s_cacheInitializedFlag, + ref s_cacheInitializedLock, + () => + { + _publishedSnapshotServiceEventHandler.Start(); + return true; + }); } } From 827bf80d1d86bb7d60585f147442211451305009 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 14 Dec 2020 17:04:02 +1100 Subject: [PATCH 07/14] Removes EnterPreview, RefreshPreview, ExitPreview --- .../IPublishedSnapshotService.cs | 50 ------------------- .../PublishedSnapshotServiceBase.cs | 9 ---- .../PublishedSnapshotService.cs | 21 -------- .../XmlPublishedSnapshotService.cs | 26 ---------- .../Controllers/PreviewController.cs | 14 ++---- 5 files changed, 4 insertions(+), 116 deletions(-) diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs index cc526ffe6e..a953c7677e 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs @@ -77,56 +77,6 @@ namespace Umbraco.Web.PublishedCache #endregion - #region Preview - - /* Later on we can imagine that EnterPreview would handle a "level" that would be either - * the content only, or the content's branch, or the whole tree + it could be possible - * to register filters against the factory to filter out which nodes should be preview - * vs non preview. - * - * EnterPreview() returns the previewToken. It is up to callers to store that token - * wherever they want, most probably in a cookie. - * - */ - - /// - /// Enters preview for specified user and content. - /// - /// The user. - /// The content identifier. - /// A preview token. - /// - /// Tells the caches that they should prepare any data that they would be keeping - /// in order to provide preview to a given user. In the Xml cache this means creating the Xml - /// file, though other caches may do things differently. - /// Does not handle the preview token storage (cookie, etc) that must be handled separately. - /// - string EnterPreview(IUser user, int contentId); // TODO: Remove this, it is not needed and is legacy from the XML cache - - /// - /// Refreshes preview for a specified content. - /// - /// The preview token. - /// The content identifier. - /// Tells the caches that they should update any data that they would be keeping - /// in order to provide preview to a given user. In the Xml cache this means updating the Xml - /// file, though other caches may do things differently. - void RefreshPreview(string previewToken, int contentId); // TODO: Remove this, it is not needed and is legacy from the XML cache - - /// - /// Exits preview for a specified preview token. - /// - /// The preview token. - /// - /// Tells the caches that they can dispose of any data that they would be keeping - /// in order to provide preview to a given user. In the Xml cache this means deleting the Xml file, - /// though other caches may do things differently. - /// Does not handle the preview token storage (cookie, etc) that must be handled separately. - /// - void ExitPreview(string previewToken); // TODO: Remove this, it is not needed and is legacy from the XML cache - - #endregion - #region Changes /* An IPublishedCachesService implementation can rely on transaction-level events to update diff --git a/src/Umbraco.Core/PublishedCache/PublishedSnapshotServiceBase.cs b/src/Umbraco.Core/PublishedCache/PublishedSnapshotServiceBase.cs index 6a8324cc27..d334e69775 100644 --- a/src/Umbraco.Core/PublishedCache/PublishedSnapshotServiceBase.cs +++ b/src/Umbraco.Core/PublishedCache/PublishedSnapshotServiceBase.cs @@ -36,15 +36,6 @@ namespace Umbraco.Web.PublishedCache /// public abstract bool EnsureEnvironment(out IEnumerable errors); - /// - public abstract string EnterPreview(IUser user, int contentId); - - /// - public abstract void RefreshPreview(string previewToken, int contentId); - - /// - public abstract void ExitPreview(string previewToken); - /// public abstract void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged); diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs index 3011e3d655..cb5fed176e 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs @@ -1131,27 +1131,6 @@ namespace Umbraco.Web.PublishedCache.NuCache #endregion - #region Preview - - // TODO: Delete this all - public override string EnterPreview(IUser user, int contentId) - { - return "preview"; // anything - } - - public override void RefreshPreview(string previewToken, int contentId) - { - // nothing - } - - public override void ExitPreview(string previewToken) - { - // nothing - } - - #endregion - - #region Rebuild Database PreCache public override void Rebuild( diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs index 9c9e2d1da2..134f3b1938 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs @@ -169,32 +169,6 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache #endregion - #region Preview - - public override string EnterPreview(IUser user, int contentId) - { - var previewContent = new PreviewContent(_xmlStore, user.Id); - previewContent.CreatePreviewSet(contentId, true); // preview branch below that content - return previewContent.Token; - //previewContent.ActivatePreviewCookie(); - } - - public override void RefreshPreview(string previewToken, int contentId) - { - if (previewToken.IsNullOrWhiteSpace()) return; - var previewContent = new PreviewContent(_xmlStore, previewToken); - previewContent.CreatePreviewSet(contentId, true); // preview branch below that content - } - - public override void ExitPreview(string previewToken) - { - if (previewToken.IsNullOrWhiteSpace()) return; - var previewContent = new PreviewContent(_xmlStore, previewToken); - previewContent.ClearPreviewSet(); - } - - #endregion - #region Xml specific /// diff --git a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs index 3dd4191c2e..0405012898 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.Extensions.Options; @@ -108,7 +108,6 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// The endpoint that is loaded within the preview iframe /// - /// [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] public ActionResult Frame(int id, string culture) { @@ -119,22 +118,17 @@ namespace Umbraco.Web.BackOffice.Controllers return RedirectPermanent($"../../{id}.aspx{query}"); } + public ActionResult EnterPreview(int id) { var user = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; - - var previewToken = _publishedSnapshotService.EnterPreview(user, id); - - _cookieManager.SetCookieValue(Constants.Web.PreviewCookieName, previewToken); + _cookieManager.SetCookieValue(Constants.Web.PreviewCookieName, "preview"); return null; } + public ActionResult End(string redir = null) { - var previewToken = _cookieManager.GetPreviewCookieValue(); - - _publishedSnapshotService.ExitPreview(previewToken); - _cookieManager.ExpireCookie(Constants.Web.PreviewCookieName); // Expire Client-side cookie that determines whether the user has accepted to be in Preview Mode when visiting the website. From 4bb1535786bf827edd30c1ec0899c539e4b515a1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 14 Dec 2020 17:43:00 +1100 Subject: [PATCH 08/14] allows the back office to route --- src/Umbraco.Core/UriExtensions.cs | 16 ++++-------- .../Extensions/HttpRequestExtensions.cs | 23 ++++++----------- ...racoWebsiteApplicationBuilderExtensions.cs | 4 +-- .../Routing/UmbracoRouteValueTransformer.cs | 25 +++++++++++++++++-- 4 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/Umbraco.Core/UriExtensions.cs b/src/Umbraco.Core/UriExtensions.cs index 0452373d55..ea846f7f7a 100644 --- a/src/Umbraco.Core/UriExtensions.cs +++ b/src/Umbraco.Core/UriExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Linq; using Microsoft.Extensions.Logging; @@ -100,16 +100,13 @@ namespace Umbraco.Core return false; } - //if its anything else we can assume it's back office + // if its anything else we can assume it's back office return true; } /// /// Checks if the current uri is an install request /// - /// - /// - /// public static bool IsInstallerRequest(this Uri url, IHostingEnvironment hostingEnvironment) { var authority = url.GetLeftPart(UriPartial.Authority); @@ -117,18 +114,14 @@ namespace Umbraco.Core .TrimStart(authority) .TrimStart("/"); - //check if this is in the umbraco back office + // check if this is in the umbraco back office return afterAuthority.InvariantStartsWith(hostingEnvironment.ToAbsolute(Constants.SystemDirectories.Install).TrimStart("/")); } /// /// Checks if the uri is a request for the default back office page /// - /// - /// - /// - /// - internal static bool IsDefaultBackOfficeRequest(this Uri url, GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) + public static bool IsDefaultBackOfficeRequest(this Uri url, GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) { var backOfficePath = globalSettings.GetBackOfficePath(hostingEnvironment); if (url.AbsolutePath.InvariantEquals(backOfficePath.TrimEnd("/")) @@ -138,6 +131,7 @@ namespace Umbraco.Core { return true; } + return false; } diff --git a/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs index 69fae56d32..fe61941e5c 100644 --- a/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Net; using System.Text; @@ -18,27 +18,20 @@ namespace Umbraco.Extensions /// /// Check if a preview cookie exist /// - /// - /// public static bool HasPreviewCookie(this HttpRequest request) - { - return request.Cookies.TryGetValue(Constants.Web.PreviewCookieName, out var cookieVal) && !cookieVal.IsNullOrWhiteSpace(); - } + => request.Cookies.TryGetValue(Constants.Web.PreviewCookieName, out var cookieVal) && !cookieVal.IsNullOrWhiteSpace(); public static bool IsBackOfficeRequest(this HttpRequest request, GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) - { - return new Uri(request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsBackOfficeRequest(globalSettings, hostingEnvironment); - } + => new Uri(request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsBackOfficeRequest(globalSettings, hostingEnvironment); public static bool IsClientSideRequest(this HttpRequest request) - { - return new Uri(request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsClientSideRequest(); - } + => new Uri(request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsClientSideRequest(); + + public static bool IsDefaultBackOfficeRequest(this HttpRequest request, GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) + => new Uri(request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsDefaultBackOfficeRequest(globalSettings, hostingEnvironment); public static string ClientCulture(this HttpRequest request) - { - return request.Headers.TryGetValue("X-UMB-CULTURE", out var values) ? values[0] : null; - } + => request.Headers.TryGetValue("X-UMB-CULTURE", out var values) ? values[0] : null; /// /// Determines if a request is local. diff --git a/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs b/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs index 32d84088c1..438ad154ed 100644 --- a/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs @@ -38,10 +38,10 @@ namespace Umbraco.Extensions { app.UseEndpoints(endpoints => { - endpoints.MapDynamicControllerRoute("/{**slug}"); - NoContentRoutes noContentRoutes = app.ApplicationServices.GetRequiredService(); noContentRoutes.CreateRoutes(endpoints); + + endpoints.MapDynamicControllerRoute("/{**slug}"); }); return app; diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index be7c9f7409..c41f34acc6 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -4,14 +4,18 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Umbraco.Core; using Umbraco.Core.Composing; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Hosting; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Strings; using Umbraco.Extensions; @@ -42,6 +46,8 @@ namespace Umbraco.Web.Website.Routing private readonly IShortStringHelper _shortStringHelper; private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; private readonly IPublishedRouter _publishedRouter; + private readonly GlobalSettings _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; /// /// Initializes a new instance of the class. @@ -52,7 +58,9 @@ namespace Umbraco.Web.Website.Routing IUmbracoRenderingDefaults renderingDefaults, IShortStringHelper shortStringHelper, IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, - IPublishedRouter publishedRouter) + IPublishedRouter publishedRouter, + IOptions globalSettings, + IHostingEnvironment hostingEnvironment) { _logger = logger; _umbracoContextAccessor = umbracoContextAccessor; @@ -60,19 +68,32 @@ namespace Umbraco.Web.Website.Routing _shortStringHelper = shortStringHelper; _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; _publishedRouter = publishedRouter; + _globalSettings = globalSettings.Value; + _hostingEnvironment = hostingEnvironment; } /// public override async ValueTask TransformAsync(HttpContext httpContext, RouteValueDictionary values) { + // will be null for any client side requests like JS, etc... if (_umbracoContextAccessor.UmbracoContext == null) { - throw new InvalidOperationException($"There is no current UmbracoContext, it must be initialized before the {nameof(UmbracoRouteValueTransformer)} executes, ensure that {nameof(UmbracoRequestMiddleware)} is registered prior to 'UseRouting'"); + return values; + // throw new InvalidOperationException($"There is no current UmbracoContext, it must be initialized before the {nameof(UmbracoRouteValueTransformer)} executes, ensure that {nameof(UmbracoRequestMiddleware)} is registered prior to 'UseRouting'"); + } + + // Check for back office request + // TODO: This is how the module was doing it before but could just as easily be part of the RoutableDocumentFilter + // which still needs to be migrated. + if (httpContext.Request.IsDefaultBackOfficeRequest(_globalSettings, _hostingEnvironment)) + { + return values; } bool routed = RouteRequest(_umbracoContextAccessor.UmbracoContext, out IPublishedRequest publishedRequest); if (!routed) { + return values; // TODO: Deal with it not being routable, perhaps this should be an enum result? } From 47e98bfdc67729a0c5866699c0bfe35e3a060c6c Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 16 Dec 2020 16:31:23 +1100 Subject: [PATCH 09/14] Fixes up issue with UsePlugins and where it's executed --- .../BackOfficeApplicationBuilderExtensions.cs | 33 +------------------ .../ApplicationBuilderExtensions.cs | 28 ++++++++++++++++ .../UmbracoPluginPhysicalFileProvider.cs | 7 ++-- 3 files changed, 32 insertions(+), 36 deletions(-) rename src/{Umbraco.Web.BackOffice => Umbraco.Web.Common}/Plugins/UmbracoPluginPhysicalFileProvider.cs (92%) diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs index bee2854a7f..71cb14eb78 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs @@ -1,14 +1,7 @@ using System; -using System.IO; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using SixLabors.ImageSharp.Web.DependencyInjection; -using Umbraco.Core; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Hosting; using Umbraco.Web.BackOffice.Middleware; -using Umbraco.Web.BackOffice.Plugins; using Umbraco.Web.BackOffice.Routing; using Umbraco.Web.Common.Security; @@ -19,7 +12,6 @@ namespace Umbraco.Extensions /// public static class BackOfficeApplicationBuilderExtensions { - app.UseUmbracoPlugins(); public static IApplicationBuilder UseUmbracoBackOffice(this IApplicationBuilder app) { // NOTE: This method will have been called after UseRouting, UseAuthentication, UseAuthorization @@ -50,30 +42,6 @@ namespace Umbraco.Extensions return app; } - public static IApplicationBuilder UseUmbracoPlugins(this IApplicationBuilder app) - { - var hostingEnvironment = app.ApplicationServices.GetRequiredService(); - var umbracoPluginSettings = app.ApplicationServices.GetRequiredService>(); - - var pluginFolder = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.AppPlugins); - - // Ensure the plugin folder exists - Directory.CreateDirectory(pluginFolder); - - var fileProvider = new UmbracoPluginPhysicalFileProvider( - pluginFolder, - umbracoPluginSettings); - - app.UseStaticFiles(new StaticFileOptions - { - FileProvider = fileProvider, - RequestPath = Constants.SystemDirectories.AppPlugins - }); - - return app; - } - - public static IApplicationBuilder UseUmbracoPreview(this IApplicationBuilder app) { // TODO: I'm unsure this middleware will execute before the endpoint, we'll have to see @@ -87,6 +55,7 @@ namespace Umbraco.Extensions return app; } + private static IApplicationBuilder UseBackOfficeUserManagerAuditing(this IApplicationBuilder app) { var auditer = app.ApplicationServices.GetRequiredService(); diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 655867ebeb..4eb26ce789 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -1,16 +1,20 @@ using System; +using System.IO; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Serilog.Context; using SixLabors.ImageSharp.Web.DependencyInjection; using Smidge; using Smidge.Nuglify; using StackExchange.Profiling; using Umbraco.Core; +using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; using Umbraco.Infrastructure.Logging.Serilog.Enrichers; using Umbraco.Web.Common.Middleware; +using Umbraco.Web.Common.Plugins; using Umbraco.Web.PublishedCache.NuCache; namespace Umbraco.Extensions @@ -44,6 +48,7 @@ namespace Umbraco.Extensions // TODO: Since we are dependent on these we need to register them but what happens when we call this multiple times since we are dependent on this for UseUmbracoBackOffice too? app.UseImageSharp(); app.UseStaticFiles(); + app.UseUmbracoPlugins(); // UseRouting adds endpoint routing middleware, this means that middlewares registered after this one // will execute after endpoint routing. The ordering of everything is quite important here, see @@ -177,6 +182,29 @@ namespace Umbraco.Extensions return app; } + public static IApplicationBuilder UseUmbracoPlugins(this IApplicationBuilder app) + { + var hostingEnvironment = app.ApplicationServices.GetRequiredService(); + var umbracoPluginSettings = app.ApplicationServices.GetRequiredService>(); + + var pluginFolder = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.AppPlugins); + + // Ensure the plugin folder exists + Directory.CreateDirectory(pluginFolder); + + var fileProvider = new UmbracoPluginPhysicalFileProvider( + pluginFolder, + umbracoPluginSettings); + + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = fileProvider, + RequestPath = Constants.SystemDirectories.AppPlugins + }); + + return app; + } + /// /// Ensures the runtime is shutdown when the application is shutting down /// diff --git a/src/Umbraco.Web.BackOffice/Plugins/UmbracoPluginPhysicalFileProvider.cs b/src/Umbraco.Web.Common/Plugins/UmbracoPluginPhysicalFileProvider.cs similarity index 92% rename from src/Umbraco.Web.BackOffice/Plugins/UmbracoPluginPhysicalFileProvider.cs rename to src/Umbraco.Web.Common/Plugins/UmbracoPluginPhysicalFileProvider.cs index 42300e3b71..d62e203cce 100644 --- a/src/Umbraco.Web.BackOffice/Plugins/UmbracoPluginPhysicalFileProvider.cs +++ b/src/Umbraco.Web.Common/Plugins/UmbracoPluginPhysicalFileProvider.cs @@ -1,14 +1,13 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System.IO; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders.Physical; using Microsoft.Extensions.Options; -using Umbraco.Core; using Umbraco.Core.Configuration.Models; -namespace Umbraco.Web.BackOffice.Plugins +namespace Umbraco.Web.Common.Plugins { /// /// Looks up files using the on-disk file system and check file extensions are on a allow list @@ -41,7 +40,7 @@ namespace Umbraco.Web.BackOffice.Plugins public new IFileInfo GetFileInfo(string subpath) { var extension = Path.GetExtension(subpath); - var subPathInclAppPluginsFolder = Path.Combine(Constants.SystemDirectories.AppPlugins, subpath); + var subPathInclAppPluginsFolder = Path.Combine(Core.Constants.SystemDirectories.AppPlugins, subpath); if (!_options.Value.BrowsableFileExtensions.Contains(extension)) { return new NotFoundFileInfo(subPathInclAppPluginsFolder); From 868c9d02df2905ea3e38ac325ee3ae48d542bd0d Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 16 Dec 2020 16:39:06 +1100 Subject: [PATCH 10/14] Fixes routing when installer needs to run --- .../Routing/UmbracoRouteValueTransformer.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index c41f34acc6..731c0320d6 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -48,6 +48,7 @@ namespace Umbraco.Web.Website.Routing private readonly IPublishedRouter _publishedRouter; private readonly GlobalSettings _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; + private readonly IRuntimeState _runtime; /// /// Initializes a new instance of the class. @@ -60,7 +61,8 @@ namespace Umbraco.Web.Website.Routing IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, IPublishedRouter publishedRouter, IOptions globalSettings, - IHostingEnvironment hostingEnvironment) + IHostingEnvironment hostingEnvironment, + IRuntimeState runtime) { _logger = logger; _umbracoContextAccessor = umbracoContextAccessor; @@ -70,16 +72,22 @@ namespace Umbraco.Web.Website.Routing _publishedRouter = publishedRouter; _globalSettings = globalSettings.Value; _hostingEnvironment = hostingEnvironment; + _runtime = runtime; } /// public override async ValueTask TransformAsync(HttpContext httpContext, RouteValueDictionary values) { + // If we aren't running, then we have nothing to route + if (_runtime.Level != RuntimeLevel.Run) + { + return values; + } + // will be null for any client side requests like JS, etc... if (_umbracoContextAccessor.UmbracoContext == null) { return values; - // throw new InvalidOperationException($"There is no current UmbracoContext, it must be initialized before the {nameof(UmbracoRouteValueTransformer)} executes, ensure that {nameof(UmbracoRequestMiddleware)} is registered prior to 'UseRouting'"); } // Check for back office request From cc1404747b2437dd3c20a2b4f9ddf166bf2b432d Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 17 Dec 2020 16:27:28 +1100 Subject: [PATCH 11/14] Changes PublishedSnapshotService to lazily load it's caches on demand when they are required instead of relying on an external initializer to load them. --- .../IPublishedSnapshotService.cs | 25 +-- .../PublishedSnapshotServiceBase.cs | 9 +- ...UmbracoContextPublishedSnapshotAccessor.cs | 7 +- .../Persistence/INuCacheContentService.cs | 2 +- .../Property.cs | 3 +- .../PublishedContent.cs | 4 +- .../PublishedSnapshotService.cs | 210 ++++++++---------- .../PublishedSnapshotServiceEventHandler.cs | 4 - .../Controllers/ContentControllerTests.cs | 25 +-- .../UmbracoTestServerTestBase.cs | 12 +- .../XmlPublishedSnapshotService.cs | 2 +- 11 files changed, 131 insertions(+), 172 deletions(-) diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs index a953c7677e..af8f72ce6d 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs @@ -24,13 +24,9 @@ namespace Umbraco.Web.PublishedCache */ /// - /// Loads the caches on startup - called once during startup - /// TODO: Temporary, this is temporal coupling, we cannot use IUmbracoApplicationLifetime.ApplicationInit (which we want to delete) - /// handler because that is executed with netcore's IHostApplicationLifetime.ApplicationStarted mechanism which fires async - /// which we don't want since this will not have initialized before our endpoints execute. So for now this is explicitly - /// called on UseUmbracoContentCaching on startup. + /// Gets the published snapshot accessor. /// - void LoadCachesOnStartup(); + IPublishedSnapshotAccessor PublishedSnapshotAccessor { get; } /// /// Creates a published snapshot. @@ -42,11 +38,6 @@ namespace Umbraco.Web.PublishedCache /// which is not specified and depends on the actual published snapshot service implementation. IPublishedSnapshot CreatePublishedSnapshot(string previewToken); - /// - /// Gets the published snapshot accessor. - /// - IPublishedSnapshotAccessor PublishedSnapshotAccessor { get; } - /// /// Ensures that the published snapshot has the proper environment to run. /// @@ -54,17 +45,15 @@ namespace Umbraco.Web.PublishedCache /// A value indicating whether the published snapshot has the proper environment to run. bool EnsureEnvironment(out IEnumerable errors); - #region Rebuild - /// - /// Rebuilds internal caches (but does not reload). + /// Rebuilds internal database caches (but does not reload). /// /// The operation batch size to process the items /// If not null will process content for the matching content types, if empty will process all content /// If not null will process content for the matching media types, if empty will process all media /// If not null will process content for the matching members types, if empty will process all members /// - /// Forces the snapshot service to rebuild its internal caches. For instance, some caches + /// Forces the snapshot service to rebuild its internal database caches. For instance, some caches /// may rely on a database table to store pre-serialized version of documents. /// This does *not* reload the caches. Caches need to be reloaded, for instance via /// RefreshAllPublishedSnapshot method. @@ -75,10 +64,6 @@ namespace Umbraco.Web.PublishedCache IReadOnlyCollection mediaTypeIds = null, IReadOnlyCollection memberTypeIds = null); - #endregion - - #region Changes - /* An IPublishedCachesService implementation can rely on transaction-level events to update * its internal, database-level data, as these events are purely internal. However, it cannot * rely on cache refreshers CacheUpdated events to update itself, as these events are external @@ -123,8 +108,6 @@ namespace Umbraco.Web.PublishedCache /// The changes. void Notify(DomainCacheRefresher.JsonPayload[] payloads); - #endregion - // TODO: This is weird, why is this is this a thing? Maybe IPublishedSnapshotStatus? string GetStatus(); diff --git a/src/Umbraco.Core/PublishedCache/PublishedSnapshotServiceBase.cs b/src/Umbraco.Core/PublishedCache/PublishedSnapshotServiceBase.cs index d334e69775..f33eb61e8f 100644 --- a/src/Umbraco.Core/PublishedCache/PublishedSnapshotServiceBase.cs +++ b/src/Umbraco.Core/PublishedCache/PublishedSnapshotServiceBase.cs @@ -6,6 +6,7 @@ using Umbraco.Web.Cache; namespace Umbraco.Web.PublishedCache { + // TODO: This base class probably shouldn't exist public abstract class PublishedSnapshotServiceBase : IPublishedSnapshotService { /// @@ -51,15 +52,12 @@ namespace Umbraco.Web.PublishedCache /// public abstract void Notify(DomainCacheRefresher.JsonPayload[] payloads); - // TODO: Why is this virtual? - /// - public virtual void Rebuild( + public abstract void Rebuild( int groupSize = 5000, IReadOnlyCollection contentTypeIds = null, IReadOnlyCollection mediaTypeIds = null, - IReadOnlyCollection memberTypeIds = null) - { } + IReadOnlyCollection memberTypeIds = null); /// public virtual void Dispose() @@ -76,6 +74,5 @@ namespace Umbraco.Web.PublishedCache { } - public abstract void LoadCachesOnStartup(); } } diff --git a/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs b/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs index 7e0ebc6f13..874da1f3aa 100644 --- a/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs +++ b/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs @@ -1,6 +1,11 @@ -using System; +using System; namespace Umbraco.Web.PublishedCache { + // TODO: This is a mess. This is a circular reference: + // IPublishedSnapshotAccessor -> PublishedSnapshotService -> UmbracoContext -> PublishedSnapshotService -> IPublishedSnapshotAccessor + // Injecting IPublishedSnapshotAccessor into PublishedSnapshotService seems pretty strange + // The underlying reason for this mess is because IPublishedContent is both a service and a model. + // Until that is fixed, IPublishedContent will need to have a IPublishedSnapshotAccessor public class UmbracoContextPublishedSnapshotAccessor : IPublishedSnapshotAccessor { private readonly IUmbracoContextAccessor _umbracoContextAccessor; diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentService.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentService.cs index 0ac3939742..4a3f5b2b5d 100644 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentService.cs +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentService.cs @@ -75,7 +75,7 @@ namespace Umbraco.Infrastructure.PublishedCache.Persistence void RefreshEntity(IContentBase content); /// - /// Rebuilds the caches for content, media and/or members based on the content type ids specified + /// Rebuilds the database caches for content, media and/or members based on the content type ids specified /// /// The operation batch size to process the items /// If not null will process content for the matching content types, if empty will process all content diff --git a/src/Umbraco.PublishedCache.NuCache/Property.cs b/src/Umbraco.PublishedCache.NuCache/Property.cs index 86023bb302..1b70c6504c 100644 --- a/src/Umbraco.PublishedCache.NuCache/Property.cs +++ b/src/Umbraco.PublishedCache.NuCache/Property.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Xml.Serialization; using Umbraco.Core; @@ -158,6 +158,7 @@ namespace Umbraco.Web.PublishedCache.NuCache default: throw new InvalidOperationException("Invalid cache level."); } + return cacheValues; } diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedContent.cs b/src/Umbraco.PublishedCache.NuCache/PublishedContent.cs index 6fe65a4ff5..9cdc0db4fa 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedContent.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedContent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; @@ -43,7 +43,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // add one property per property type - this is required, for the indexing to work // if contentData supplies pdatas, use them, else use null contentData.Properties.TryGetValue(propertyType.Alias, out var pdatas); // else will be null - properties[i++] =new Property(propertyType, this, pdatas, _publishedSnapshotAccessor); + properties[i++] = new Property(propertyType, this, pdatas, _publishedSnapshotAccessor); } PropertiesArray = properties; } diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs index cb5fed176e..6225e68ec6 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Threading; using CSharpTest.Net.Collections; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -46,7 +47,9 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly NuCacheSettings _config; // volatile because we read it with no lock - private volatile bool _isReady; + private bool _isReady; + private bool _isReadSet; + private object _isReadyLock; private readonly ContentStore _contentStore; private readonly ContentStore _mediaStore; @@ -139,20 +142,20 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - #region Id <-> Key methods - // NOTE: These aren't used within this object but are made available internally to improve the IdKey lookup performance // when nucache is enabled. - // TODO: Does this need to be here? - + // TODO: Does this need to be here? internal int GetDocumentId(Guid udi) => GetId(_contentStore, udi); - internal int GetMediaId(Guid udi) => GetId(_mediaStore, udi); - internal Guid GetDocumentUid(int id) => GetUid(_contentStore, id); - internal Guid GetMediaUid(int id) => GetUid(_mediaStore, id); - private int GetId(ContentStore store, Guid uid) => store.LiveSnapshot.Get(uid)?.Id ?? 0; - private Guid GetUid(ContentStore store, int id) => store.LiveSnapshot.Get(id)?.Uid ?? Guid.Empty; - #endregion + internal int GetMediaId(Guid udi) => GetId(_mediaStore, udi); + + internal Guid GetDocumentUid(int id) => GetUid(_contentStore, id); + + internal Guid GetMediaUid(int id) => GetUid(_mediaStore, id); + + private int GetId(ContentStore store, Guid uid) => store.LiveSnapshot.Get(uid)?.Id ?? 0; + + private Guid GetUid(ContentStore store, int id) => store.LiveSnapshot.Get(id)?.Uid ?? Guid.Empty; /// /// Install phase of @@ -204,66 +207,6 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - /// - /// Populates the stores - /// - public override void LoadCachesOnStartup() - { - lock (_storesLock) - { - if (_isReady) - { - throw new InvalidOperationException("The caches can only be loaded on startup one time"); - } - - var okContent = false; - var okMedia = false; - - try - { - if (_localContentDbExists) - { - okContent = LockAndLoadContent(() => LoadContentFromLocalDbLocked(true)); - if (!okContent) - { - _logger.LogWarning("Loading content from local db raised warnings, will reload from database."); - } - } - - if (_localMediaDbExists) - { - okMedia = LockAndLoadMedia(() => LoadMediaFromLocalDbLocked(true)); - if (!okMedia) - { - _logger.LogWarning("Loading media from local db raised warnings, will reload from database."); - } - } - - if (!okContent) - { - LockAndLoadContent(() => LoadContentFromDatabaseLocked(true)); - } - - if (!okMedia) - { - LockAndLoadMedia(() => LoadMediaFromDatabaseLocked(true)); - } - - LockAndLoadDomains(); - } - catch (Exception ex) - { - _logger.LogCritical(ex, "Panic, exception while loading cache data."); - throw; - } - - // finally, cache is ready! - _isReady = true; - } - } - - #region Local files - private string GetLocalFilesPath() { var path = Path.Combine(_hostingEnvironment.LocalTempPath, "NuCache"); @@ -278,7 +221,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private void DeleteLocalFilesForContent() { - if (_isReady && _localContentDb != null) + if (Volatile.Read(ref _isReady) && _localContentDb != null) { throw new InvalidOperationException("Cannot delete local files while the cache uses them."); } @@ -293,7 +236,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private void DeleteLocalFilesForMedia() { - if (_isReady && _localMediaDb != null) + if (Volatile.Read(ref _isReady) && _localMediaDb != null) { throw new InvalidOperationException("Cannot delete local files while the cache uses them."); } @@ -306,10 +249,6 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - #endregion - - #region Environment - public override bool EnsureEnvironment(out IEnumerable errors) { // must have app_data and be able to write files into it @@ -318,9 +257,62 @@ namespace Umbraco.Web.PublishedCache.NuCache return ok; } - #endregion + /// + /// Populates the stores + /// + private void EnsureCaches() => LazyInitializer.EnsureInitialized( + ref _isReady, + ref _isReadSet, + ref _isReadyLock, + () => + { + // even though we are ready locked here we want to ensure that the stores lock is also locked + lock (_storesLock) + { + var okContent = false; + var okMedia = false; - #region Populate Stores + try + { + if (_localContentDbExists) + { + okContent = LockAndLoadContent(() => LoadContentFromLocalDbLocked(true)); + if (!okContent) + { + _logger.LogWarning("Loading content from local db raised warnings, will reload from database."); + } + } + + if (_localMediaDbExists) + { + okMedia = LockAndLoadMedia(() => LoadMediaFromLocalDbLocked(true)); + if (!okMedia) + { + _logger.LogWarning("Loading media from local db raised warnings, will reload from database."); + } + } + + if (!okContent) + { + LockAndLoadContent(() => LoadContentFromDatabaseLocked(true)); + } + + if (!okMedia) + { + LockAndLoadMedia(() => LoadMediaFromDatabaseLocked(true)); + } + + LockAndLoadDomains(); + } + catch (Exception ex) + { + _logger.LogCritical(ex, "Panic, exception while loading cache data."); + throw; + } + + return true; + } + }); // sudden panic... but in RepeatableRead can a content that I haven't already read, be removed // before I read it? NO! because the WHOLE content tree is read-locked using WithReadLocked. @@ -482,10 +474,6 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - #endregion - - #region Handle Notifications - // note: if the service is not ready, ie _isReady is false, then notifications are ignored // SetUmbracoVersionStep issues a DistributedCache.Instance.RefreshAll...() call which should cause @@ -512,7 +500,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public override void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged) { // no cache, trash everything - if (_isReady == false) + if (Volatile.Read(ref _isReady) == false) { DeleteLocalFilesForContent(); draftChanged = publishedChanged = true; @@ -613,7 +601,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public override void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged) { // no cache, trash everything - if (_isReady == false) + if (Volatile.Read(ref _isReady) == false) { DeleteLocalFilesForMedia(); anythingChanged = true; @@ -711,7 +699,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public override void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads) { // no cache, nothing we can do - if (_isReady == false) + if (Volatile.Read(ref _isReady) == false) { return; } @@ -812,7 +800,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public override void Notify(DataTypeCacheRefresher.JsonPayload[] payloads) { // no cache, nothing we can do - if (_isReady == false) + if (Volatile.Read(ref _isReady) == false) { return; } @@ -856,7 +844,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public override void Notify(DomainCacheRefresher.JsonPayload[] payloads) { // no cache, nothing we can do - if (_isReady == false) + if (Volatile.Read(ref _isReady) == false) { return; } @@ -894,12 +882,9 @@ namespace Umbraco.Web.PublishedCache.NuCache // Methods used to prevent allocations of lists private void AddToList(ref List list, int val) => GetOrCreateList(ref list).Add(val); + private List GetOrCreateList(ref List list) => list ?? (list = new List()); - #endregion - - #region Content Types - private IReadOnlyCollection CreateContentTypes(PublishedItemType itemType, int[] ids) { // XxxTypeService.GetAll(empty) returns everything! @@ -1028,14 +1013,12 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - #endregion - - #region Create, Get Published Snapshot - public override IPublishedSnapshot CreatePublishedSnapshot(string previewToken) { + EnsureCaches(); + // no cache, no joy - if (_isReady == false) + if (Volatile.Read(ref _isReady) == false) { throw new InvalidOperationException("The published snapshot service has not properly initialized."); } @@ -1049,6 +1032,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // even though the underlying elements may not change (store snapshots) public PublishedSnapshot.PublishedSnapshotElements GetElements(bool previewDefault) { + EnsureCaches(); + // note: using ObjectCacheAppCache for elements and snapshot caches // is not recommended because it creates an inner MemoryCache which is a heavy // thing - better use a dictionary-based cache which "just" creates a concurrent @@ -1129,10 +1114,7 @@ namespace Umbraco.Web.PublishedCache.NuCache }; } - #endregion - - #region Rebuild Database PreCache - + /// public override void Rebuild( int groupSize = 5000, IReadOnlyCollection contentTypeIds = null, @@ -1176,12 +1158,10 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - #endregion - - #region Instrument - public override string GetStatus() { + EnsureCaches(); + var dbCacheIsOk = VerifyContentDbCache() && VerifyMediaDbCache() && VerifyMemberDbCache(); @@ -1203,20 +1183,26 @@ namespace Umbraco.Web.PublishedCache.NuCache " and " + ms + " snapshot" + (ms > 1 ? "s" : "") + "."; } + // TODO: This should be async since it's calling into async public override void Collect() { + EnsureCaches(); + var contentCollect = _contentStore.CollectAsync(); var mediaCollect = _mediaStore.CollectAsync(); System.Threading.Tasks.Task.WaitAll(contentCollect, mediaCollect); } - #endregion + internal ContentStore GetContentStore() + { + EnsureCaches(); + return _contentStore; + } - #region Internals/Testing - - internal ContentStore GetContentStore() => _contentStore; - internal ContentStore GetMediaStore() => _mediaStore; - - #endregion + internal ContentStore GetMediaStore() + { + EnsureCaches(); + return _mediaStore; + } } } diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs index 20ce74ea70..31bc9b3d63 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs @@ -40,10 +40,6 @@ namespace Umbraco.Web.PublishedCache.NuCache return false; } - // this initializes the caches. - // TODO: This is still temporal coupling (i.e. Initialize) - _publishedSnapshotService.LoadCachesOnStartup(); - // we always want to handle repository events, configured or not // assuming no repository event will trigger before the whole db is ready // (ideally we'd have Upgrading.App vs Upgrading.Data application states...) diff --git a/src/Umbraco.Tests.Integration/TestServerTest/Controllers/ContentControllerTests.cs b/src/Umbraco.Tests.Integration/TestServerTest/Controllers/ContentControllerTests.cs index 731079da7c..dbac7c9f76 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/Controllers/ContentControllerTests.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/Controllers/ContentControllerTests.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -22,13 +22,12 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers /// /// Returns 404 if the content wasn't found based on the ID specified /// - /// [Test] public async Task PostSave_Validate_Existing_Content() { var localizationService = GetRequiredService(); - //Add another language + // Add another language localizationService.Save(new LanguageBuilder() .WithCultureInfo("da-DK") .WithIsDefault(false) @@ -76,10 +75,10 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers // Assert Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); -// Assert.AreEqual(")]}',\n{\"Message\":\"content was not found\"}", response.Item1.Content.ReadAsStringAsync().Result); -// -// //var obj = JsonConvert.DeserializeObject>(response.Item2); -// //Assert.AreEqual(0, obj.TotalItems); + // Assert.AreEqual(")]}',\n{\"Message\":\"content was not found\"}", response.Item1.Content.ReadAsStringAsync().Result); + // + // //var obj = JsonConvert.DeserializeObject>(response.Item2); + // //Assert.AreEqual(0, obj.TotalItems); } [Test] @@ -88,7 +87,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers var localizationService = GetRequiredService(); - //Add another language + // Add another language localizationService.Save(new LanguageBuilder() .WithCultureInfo("da-DK") .WithIsDefault(false) @@ -96,7 +95,6 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers var url = PrepareUrl(x => x.PostSave(null)); - var contentTypeService = GetRequiredService(); var contentType = new ContentTypeBuilder() @@ -128,7 +126,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers .Build(); // HERE we force the test to fail - model.Variants = model.Variants.Select(x=> + model.Variants = model.Variants.Select(x => { x.Save = false; return x; @@ -141,7 +139,6 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers }); // Assert - var body = await response.Content.ReadAsStringAsync(); Assert.Multiple(() => @@ -155,13 +152,12 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers /// /// Returns 404 if any of the posted properties dont actually exist /// - /// [Test] public async Task PostSave_Validate_Properties_Exist() { var localizationService = GetRequiredService(); - //Add another language + // Add another language localizationService.Save(new LanguageBuilder() .WithCultureInfo("da-DK") .WithIsDefault(false) @@ -215,12 +211,11 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers }); // Assert - var body = await response.Content.ReadAsStringAsync(); body = body.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix); - Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); + Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); } [Test] diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index 93769eaaed..d0bc38ea0b 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -1,4 +1,4 @@ - + using System; using System.Linq.Expressions; using System.Net.Http; @@ -116,20 +116,20 @@ namespace Umbraco.Tests.Integration.TestServerTest } protected HttpClient Client { get; private set; } + protected LinkGenerator LinkGenerator { get; private set; } + protected WebApplicationFactory Factory { get; private set; } [TearDown] public override void TearDown() { base.TearDown(); - base.TerminateCoreRuntime(); + TerminateCoreRuntime(); Factory.Dispose(); } - #region IStartup - public override void ConfigureServices(IServiceCollection services) { services.AddTransient(); @@ -160,9 +160,5 @@ namespace Umbraco.Tests.Integration.TestServerTest { app.UseUmbraco(); } - - #endregion - - } } diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs index 134f3b1938..5e05e31708 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs @@ -253,6 +253,6 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache return "Test status"; } - public override void LoadCachesOnStartup() { } + public override void Rebuild(int groupSize = 5000, IReadOnlyCollection contentTypeIds = null, IReadOnlyCollection mediaTypeIds = null, IReadOnlyCollection memberTypeIds = null) { } } } From 75796e3eae279773759ea21c24fee912906e9869 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 21 Dec 2020 15:58:47 +1100 Subject: [PATCH 12/14] Fixing tests, removing old files, adds notes --- .../Controllers/ContentControllerTests.cs | 2 +- .../UmbracoTestServerTestBase.cs | 33 ++++--- .../ControllerTesting/TestRunner.cs | 95 ------------------- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 - .../Controllers/ContentController.cs | 28 +++--- .../Controllers/ContentControllerBase.cs | 35 +++++-- .../Filters/JsonDateTimeFormatAttribute.cs | 3 +- src/Umbraco.Web/Umbraco.Web.csproj | 2 - .../WebApi/AngularJsonMediaTypeFormatter.cs | 57 ----------- .../AngularJsonOnlyConfigurationAttribute.cs | 24 ----- 10 files changed, 68 insertions(+), 212 deletions(-) delete mode 100644 src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs delete mode 100644 src/Umbraco.Web/WebApi/AngularJsonMediaTypeFormatter.cs delete mode 100644 src/Umbraco.Web/WebApi/AngularJsonOnlyConfigurationAttribute.cs diff --git a/src/Umbraco.Tests.Integration/TestServerTest/Controllers/ContentControllerTests.cs b/src/Umbraco.Tests.Integration/TestServerTest/Controllers/ContentControllerTests.cs index dbac7c9f76..f9d5d9f7da 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/Controllers/ContentControllerTests.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/Controllers/ContentControllerTests.cs @@ -144,7 +144,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers Assert.Multiple(() => { Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); - Assert.AreEqual(")]}',\n{\"Message\":\"No variants flagged for saving\"}", body); + Assert.AreEqual(AngularJsonMediaTypeFormatter.XsrfPrefix + "{\"Message\":\"No variants flagged for saving\"}", body); }); } diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index d0bc38ea0b..1f9283b9f3 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -2,7 +2,6 @@ using System; using System.Linq.Expressions; using System.Net.Http; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -11,20 +10,18 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.DependencyInjection; using Umbraco.Extensions; using Umbraco.Tests.Integration.Testing; using Umbraco.Tests.Testing; using Umbraco.Web; -using Umbraco.Core.DependencyInjection; -using Umbraco.Web.Common.Controllers; -using Microsoft.Extensions.Hosting; -using Umbraco.Core.Cache; -using Umbraco.Core.Persistence; -using Umbraco.Core.Runtime; using Umbraco.Web.BackOffice.Controllers; +using Umbraco.Web.Common.Controllers; +using Umbraco.Web.Website.Controllers; namespace Umbraco.Tests.Integration.TestServerTest { @@ -133,8 +130,14 @@ namespace Umbraco.Tests.Integration.TestServerTest public override void ConfigureServices(IServiceCollection services) { services.AddTransient(); - var typeLoader = services.AddTypeLoader(GetType().Assembly, TestHelper.GetWebHostEnvironment(), TestHelper.GetHostingEnvironment(), - TestHelper.ConsoleLoggerFactory, AppCaches.NoCache, Configuration, TestHelper.Profiler); + var typeLoader = services.AddTypeLoader( + GetType().Assembly, + TestHelper.GetWebHostEnvironment(), + TestHelper.GetHostingEnvironment(), + TestHelper.ConsoleLoggerFactory, + AppCaches.NoCache, + Configuration, + TestHelper.Profiler); var builder = new UmbracoBuilder(services, Configuration, typeLoader); @@ -147,10 +150,16 @@ namespace Umbraco.Tests.Integration.TestServerTest .AddBackOfficeIdentity() .AddBackOfficeAuthorizationPolicies(TestAuthHandler.TestAuthenticationScheme) .AddPreviewSupport() - //.WithMiniProfiler() // we don't want this running in tests .AddMvcAndRazor(mvcBuilding: mvcBuilder => { + // Adds Umbraco.Web.BackOffice mvcBuilder.AddApplicationPart(typeof(ContentController).Assembly); + + // Adds Umbraco.Web.Common + mvcBuilder.AddApplicationPart(typeof(RenderController).Assembly); + + // Adds Umbraco.Web.Website + mvcBuilder.AddApplicationPart(typeof(SurfaceController).Assembly); }) .AddWebServer() .Build(); @@ -159,6 +168,8 @@ namespace Umbraco.Tests.Integration.TestServerTest public override void Configure(IApplicationBuilder app) { app.UseUmbraco(); + app.UseUmbracoBackOffice(); + app.UseUmbracoWebsite(); } } } diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs deleted file mode 100644 index 34b649d3bb..0000000000 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; -using System.Web.Http; -using Microsoft.Owin.Testing; -using Newtonsoft.Json; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Web; -using Umbraco.Web.WebApi; - -namespace Umbraco.Tests.TestHelpers.ControllerTesting -{ - public class TestRunner - { - private readonly Func _controllerFactory; - - public TestRunner(Func controllerFactory) - { - _controllerFactory = controllerFactory; - } - - public async Task> Execute(string controllerName, string actionName, HttpMethod method, - HttpContent content = null, - MediaTypeWithQualityHeaderValue mediaTypeHeader = null, - bool assertOkResponse = true, object routeDefaults = null, string url = null) - { - if (mediaTypeHeader == null) - { - mediaTypeHeader = new MediaTypeWithQualityHeaderValue("application/json"); - } - if (routeDefaults == null) - { - routeDefaults = new { controller = controllerName, action = actionName, id = RouteParameter.Optional }; - } - - var startup = new TestStartup( - configuration => - { - configuration.Routes.MapHttpRoute("Default", - routeTemplate: "{controller}/{action}/{id}", - defaults: routeDefaults); - }, - _controllerFactory); - - using (var server = TestServer.Create(builder => startup.Configuration(builder))) - { - var request = new HttpRequestMessage - { - RequestUri = new Uri("https://testserver/" + (url ?? "")), - Method = method - }; - - if (content != null) - request.Content = content; - - request.Headers.Accept.Add(mediaTypeHeader); - - Console.WriteLine(request); - var response = await server.HttpClient.SendAsync(request); - Console.WriteLine(response); - - if (response.IsSuccessStatusCode == false) - { - WriteResponseError(response); - } - - var json = (await ((StreamContent)response.Content).ReadAsStringAsync()).TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix); - if (!json.IsNullOrWhiteSpace()) - { - var deserialized = JsonConvert.DeserializeObject(json); - Console.Write(JsonConvert.SerializeObject(deserialized, Formatting.Indented)); - } - - if (assertOkResponse) - { - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); - } - - return Tuple.Create(response, json); - } - } - - private static void WriteResponseError(HttpResponseMessage response) - { - var result = response.Content.ReadAsStringAsync().Result; - Console.Out.WriteLine("Http operation unsuccessfull"); - Console.Out.WriteLine($"Status: '{response.StatusCode}'"); - Console.Out.WriteLine($"Reason: '{response.ReasonPhrase}'"); - Console.Out.WriteLine(result); - } - } -} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 2a7ada4c15..2d5fb4fa1b 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -174,7 +174,6 @@ - diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index a6f6bc8fb8..74856f4d1b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -636,19 +636,20 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Saves content /// - /// [FileUploadCleanupFilter] [ContentSaveValidation] public async Task PostSaveBlueprint([ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) { - var contentItemDisplay = await PostSaveInternal(contentItem, + var contentItemDisplay = await PostSaveInternal( + contentItem, content => { EnsureUniqueName(content.Name, content, "Name"); _contentService.SaveBlueprint(contentItem.PersistedContent, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); - //we need to reuse the underlying logic so return the result that it wants - return OperationResult.Succeed(new EventMessages()); + + // we need to reuse the underlying logic so return the result that it wants + return OperationResult.Succeed(new EventMessages()); }, content => { @@ -663,7 +664,6 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Saves content /// - /// [FileUploadCleanupFilter] [ContentSaveValidation] [OutgoingEditorModelEvent] @@ -679,9 +679,9 @@ namespace Umbraco.Web.BackOffice.Controllers private async Task PostSaveInternal(ContentItemSave contentItem, Func saveMethod, Func mapToDisplay) { - //Recent versions of IE/Edge may send in the full client side file path instead of just the file name. - //To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all - //uploaded files to being *only* the actual file name (as it should be). + // Recent versions of IE/Edge may send in the full client side file path instead of just the file name. + // To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all + // uploaded files to being *only* the actual file name (as it should be). if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any()) { foreach (var file in contentItem.UploadedFiles) @@ -690,7 +690,7 @@ namespace Umbraco.Web.BackOffice.Controllers } } - //If we've reached here it means: + // If we've reached here it means: // * Our model has been bound // * and validated // * any file attachments have been saved to their temporary location for us to use @@ -700,20 +700,20 @@ namespace Umbraco.Web.BackOffice.Controllers var passesCriticalValidationRules = ValidateCriticalData(contentItem, out var variantCount); - //we will continue to save if model state is invalid, however we cannot save if critical data is missing. + // we will continue to save if model state is invalid, however we cannot save if critical data is missing. if (!ModelState.IsValid) { - //check for critical data validation issues, we can't continue saving if this data is invalid + // check for critical data validation issues, we can't continue saving if this data is invalid if (!passesCriticalValidationRules) { - //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! + // ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! // add the model state to the outgoing object and throw a validation message var forDisplay = mapToDisplay(contentItem.PersistedContent); forDisplay.Errors = ModelState.ToErrorDictionary(); throw HttpResponseException.CreateValidationErrorResponse(forDisplay); } - //if there's only one variant and the model state is not valid we cannot publish so change it to save + // if there's only one variant and the model state is not valid we cannot publish so change it to save if (variantCount == 1) { switch (contentItem.Action) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs index aef6abdd5e..50012c7921 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Net; using System.Net.Http; @@ -29,14 +29,12 @@ namespace Umbraco.Web.BackOffice.Controllers [JsonDateTimeFormat] public abstract class ContentControllerBase : BackOfficeNotificationsController { - protected ICultureDictionary CultureDictionary { get; } - protected ILoggerFactory LoggerFactory { get; } - protected IShortStringHelper ShortStringHelper { get; } - protected IEventMessagesFactory EventMessages { get; } - protected ILocalizedTextService LocalizedTextService { get; } private readonly ILogger _logger; private readonly IJsonSerializer _serializer; + /// + /// Initializes a new instance of the class. + /// protected ContentControllerBase( ICultureDictionary cultureDictionary, ILoggerFactory loggerFactory, @@ -54,6 +52,31 @@ namespace Umbraco.Web.BackOffice.Controllers _serializer = serializer; } + /// + /// Gets the + /// + protected ICultureDictionary CultureDictionary { get; } + + /// + /// Gets the + /// + protected ILoggerFactory LoggerFactory { get; } + + /// + /// Gets the + /// + protected IShortStringHelper ShortStringHelper { get; } + + /// + /// Gets the + /// + protected IEventMessagesFactory EventMessages { get; } + + /// + /// Gets the + /// + protected ILocalizedTextService LocalizedTextService { get; } + protected NotFoundObjectResult HandleContentNotFound(object id, bool throwException = true) { ModelState.AddModelError("id", $"content with id: {id} was not found"); diff --git a/src/Umbraco.Web.Common/Filters/JsonDateTimeFormatAttribute.cs b/src/Umbraco.Web.Common/Filters/JsonDateTimeFormatAttribute.cs index 9c9496b282..031aeb1f4c 100644 --- a/src/Umbraco.Web.Common/Filters/JsonDateTimeFormatAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/JsonDateTimeFormatAttribute.cs @@ -1,4 +1,4 @@ -using System.Buffers; +using System.Buffers; using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -34,6 +34,7 @@ namespace Umbraco.Web.Common.Filters _arrayPool = arrayPool; _options = options; } + public void OnResultExecuted(ResultExecutedContext context) { } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index b84a003813..62060169d0 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -192,8 +192,6 @@ - - diff --git a/src/Umbraco.Web/WebApi/AngularJsonMediaTypeFormatter.cs b/src/Umbraco.Web/WebApi/AngularJsonMediaTypeFormatter.cs deleted file mode 100644 index 0e7cf6453a..0000000000 --- a/src/Umbraco.Web/WebApi/AngularJsonMediaTypeFormatter.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Formatting; -using System.Text; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Umbraco.Core.Logging; - -namespace Umbraco.Web.WebApi -{ - /// - /// This will format the JSON output for use with AngularJs's approach to JSON Vulnerability attacks - /// - /// - /// See: http://docs.angularjs.org/api/ng.$http (Security considerations) - /// - public class AngularJsonMediaTypeFormatter : JsonMediaTypeFormatter - { - - public const string XsrfPrefix = ")]}',\n"; - - /// - /// This will prepend the special chars to the stream output that angular will strip - /// - /// - /// - /// - /// - /// - /// - public override async Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext) - { - if (type == null) throw new ArgumentNullException("type"); - if (writeStream == null) throw new ArgumentNullException("writeStream"); - - var effectiveEncoding = SelectCharacterEncoding(content == null ? null : content.Headers); - - using (var streamWriter = new StreamWriter(writeStream, effectiveEncoding, - //we are only writing a few chars so we don't need to allocate a large buffer - 128, - //this is important! We don't want to close the stream, the base class is in charge of stream management, we just want to write to it. - leaveOpen:true)) - { - //write the special encoding for angular json to the start - // (see: http://docs.angularjs.org/api/ng.$http) - streamWriter.Write(XsrfPrefix); - streamWriter.Flush(); - await base.WriteToStreamAsync(type, value, writeStream, content, transportContext); - } - } - - } -} diff --git a/src/Umbraco.Web/WebApi/AngularJsonOnlyConfigurationAttribute.cs b/src/Umbraco.Web/WebApi/AngularJsonOnlyConfigurationAttribute.cs deleted file mode 100644 index 6a6e63f335..0000000000 --- a/src/Umbraco.Web/WebApi/AngularJsonOnlyConfigurationAttribute.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Linq; -using System.Net.Http.Formatting; -using System.Web.Http.Controllers; - -namespace Umbraco.Web.WebApi -{ - /// - /// Applying this attribute to any webapi controller will ensure that it only contains one json formatter compatible with the angular json vulnerability prevention. - /// - public class AngularJsonOnlyConfigurationAttribute : Attribute, IControllerConfiguration - { - public virtual void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) - { - //remove all json/xml formatters then add our custom one - var toRemove = controllerSettings.Formatters.Where(t => (t is JsonMediaTypeFormatter) || (t is XmlMediaTypeFormatter)).ToList(); - foreach (var r in toRemove) - { - controllerSettings.Formatters.Remove(r); - } - controllerSettings.Formatters.Add(new AngularJsonMediaTypeFormatter()); - } - } -} From 03f22e93626eee865562b50aa63e0c94caf43253 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 21 Dec 2020 16:44:50 +1100 Subject: [PATCH 13/14] Fixing tests after merge --- .../Runtime/CoreRuntime.cs | 2 - .../PublishedSnapshotServiceEventHandler.cs | 10 +++++ .../Testing/UmbracoIntegrationTest.cs | 31 +++++++------- .../ContentTypeServiceVariantsTests.cs | 41 +++++++++++-------- .../ApplicationBuilderExtensions.cs | 3 +- 5 files changed, 53 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 55fa3457ed..35b7443338 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -43,8 +43,6 @@ namespace Umbraco.Infrastructure.Runtime _databaseFactory = databaseFactory; _eventAggregator = eventAggregator; _hostingEnvironment = hostingEnvironment; - - _logger = _loggerFactory.CreateLogger(); } diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs index 31bc9b3d63..084ed569ca 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs @@ -11,6 +11,9 @@ using Umbraco.Infrastructure.PublishedCache.Persistence; namespace Umbraco.Web.PublishedCache.NuCache { + /// + /// Subscribes to Umbraco events to ensure nucache remains consistent with the source data + /// public class PublishedSnapshotServiceEventHandler : IDisposable { private readonly IRuntimeState _runtime; @@ -18,6 +21,9 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly IPublishedSnapshotService _publishedSnapshotService; private readonly INuCacheContentService _publishedContentService; + /// + /// Initializes a new instance of the class. + /// public PublishedSnapshotServiceEventHandler( IRuntimeState runtime, IPublishedSnapshotService publishedSnapshotService, @@ -28,6 +34,10 @@ namespace Umbraco.Web.PublishedCache.NuCache _publishedContentService = publishedContentService; } + /// + /// Binds to the Umbraco events + /// + /// Returns true if binding occurred public bool Start() { // however, the cache is NOT available until we are configured, because loading diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index d3abd76d7b..7aea7f37a5 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -50,7 +50,10 @@ namespace Umbraco.Tests.Integration.Testing public void OnTestTearDown(Action tearDown) { if (_testTeardown == null) + { _testTeardown = new List(); + } + _testTeardown.Add(tearDown); } @@ -60,7 +63,9 @@ namespace Umbraco.Tests.Integration.Testing public void FixtureTearDown() { foreach (var a in _fixtureTeardown) + { a(); + } } [TearDown] @@ -69,7 +74,9 @@ namespace Umbraco.Tests.Integration.Testing if (_testTeardown != null) { foreach (var a in _testTeardown) + { a(); + } } _testTeardown = null; @@ -107,7 +114,7 @@ namespace Umbraco.Tests.Integration.Testing Configure(app); } - protected void BeforeHostStart(IHost host) + protected virtual void BeforeHostStart(IHost host) { Services = host.Services; UseTestDatabase(Services); @@ -149,7 +156,6 @@ namespace Umbraco.Tests.Integration.Testing /// /// Create the Generic Host and execute startup ConfigureServices/Configure calls /// - /// public virtual IHostBuilder CreateHostBuilder() { var hostBuilder = Host.CreateDefaultBuilder() @@ -183,8 +189,6 @@ namespace Umbraco.Tests.Integration.Testing #endregion - #region IStartup - public virtual void ConfigureServices(IServiceCollection services) { services.AddSingleton(TestHelper.DbProviderFactoryCreator); @@ -245,8 +249,6 @@ namespace Umbraco.Tests.Integration.Testing app.UseUmbracoCore(); // This no longer starts CoreRuntime, it's very fast } - #endregion - #region LocalDb private static readonly object _dbLocker = new object(); @@ -387,8 +389,6 @@ namespace Umbraco.Tests.Integration.Testing #endregion - #region Common services - protected UmbracoTestAttribute TestOptions => TestOptionAttributeBase.GetTestOptions(); protected virtual T GetRequiredService() => Services.GetRequiredService(); @@ -420,13 +420,16 @@ namespace Umbraco.Tests.Integration.Testing /// Returns the /// protected ILoggerFactory LoggerFactory => Services.GetRequiredService(); - protected AppCaches AppCaches => Services.GetRequiredService(); - protected IIOHelper IOHelper => Services.GetRequiredService(); - protected IShortStringHelper ShortStringHelper => Services.GetRequiredService(); - protected GlobalSettings GlobalSettings => Services.GetRequiredService>().Value; - protected IMapperCollection Mappers => Services.GetRequiredService(); - #endregion + protected AppCaches AppCaches => Services.GetRequiredService(); + + protected IIOHelper IOHelper => Services.GetRequiredService(); + + protected IShortStringHelper ShortStringHelper => Services.GetRequiredService(); + + protected GlobalSettings GlobalSettings => Services.GetRequiredService>().Value; + + protected IMapperCollection Mappers => Services.GetRequiredService(); #region Builders diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs index e9ec0cbae0..1fbfb00a1b 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using NUnit.Framework; using Umbraco.Core.Cache; using Umbraco.Core.Configuration.Models; @@ -25,11 +27,33 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services public class ContentTypeServiceVariantsTests : UmbracoIntegrationTest { private ISqlContext SqlContext => GetRequiredService(); + private IContentService ContentService => GetRequiredService(); + private IContentTypeService ContentTypeService => GetRequiredService(); + private IRedirectUrlService RedirectUrlService => GetRequiredService(); + private ILocalizationService LocalizationService => GetRequiredService(); + protected override void BeforeHostStart(IHost host) + { + base.BeforeHostStart(host); + + // Ensure that the events are bound on each test + PublishedSnapshotServiceEventHandler eventBinder = host.Services.GetRequiredService(); + eventBinder.Start(); + } + + public override void TearDown() + { + // Ensure this is dipsosed + // TODO: How come MSDI doesn't automatically dispose all of these at the end of each test? + // How can we automatically dispose of all IDisposables at the end of the tests as if it were an aspnet app? + Services.GetRequiredService().Dispose(); + base.TearDown(); + } + private void AssertJsonStartsWith(int id, string expected) { var json = GetJson(id).Replace('"', '\''); @@ -512,9 +536,6 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services { // one simple content type, variant, with both variant and invariant properties // can change it to invariant and back - - //hack to ensure events are initialized - (GetRequiredService() as PublishedSnapshotService)?.OnApplicationInit(null, null); CreateFrenchAndEnglishLangs(); var contentType = CreateContentType(ContentVariation.Culture); @@ -603,10 +624,6 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services // one simple content type, invariant // can change it to variant and back // can then switch one property to variant - - //hack to ensure events are initialized - (GetRequiredService() as PublishedSnapshotService)?.OnApplicationInit(null, null); - var globalSettings = new GlobalSettings(); var languageEn = new Language(globalSettings, "en") { IsDefault = true }; @@ -696,9 +713,6 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services { // one simple content type, variant, with both variant and invariant properties // can change an invariant property to variant and back - - //hack to ensure events are initialized - (GetRequiredService() as PublishedSnapshotService)?.OnApplicationInit(null, null); CreateFrenchAndEnglishLangs(); var contentType = CreateContentType(ContentVariation.Culture); @@ -893,7 +907,6 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services { // one simple content type, variant, with both variant and invariant properties // can change an invariant property to variant and back - CreateFrenchAndEnglishLangs(); var contentType = CreateContentType(ContentVariation.Culture | ContentVariation.Segment); @@ -974,9 +987,6 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services // one composed content type, variant, with both variant and invariant properties // can change the composing content type to invariant and back // can change the composed content type to invariant and back - - //hack to ensure events are initialized - (GetRequiredService() as PublishedSnapshotService)?.OnApplicationInit(null, null); CreateFrenchAndEnglishLangs(); var composing = CreateContentType(ContentVariation.Culture, "composing"); @@ -1071,9 +1081,6 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services // one composed content type, invariant // can change the composing content type to invariant and back // can change the variant composed content type to invariant and back - - //hack to ensure events are initialized - (GetRequiredService() as PublishedSnapshotService)?.OnApplicationInit(null, null); CreateFrenchAndEnglishLangs(); var composing = CreateContentType(ContentVariation.Culture, "composing"); diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index a118fa746c..0f2d1ffcdd 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -10,6 +10,7 @@ using Smidge.Nuglify; using StackExchange.Profiling; using Umbraco.Core; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Hosting; using Umbraco.Infrastructure.Logging.Serilog.Enrichers; using Umbraco.Web.Common.Middleware; using Umbraco.Web.Common.Plugins; @@ -79,7 +80,7 @@ namespace Umbraco.Extensions } /// - /// Start Umbraco + /// Enables core Umbraco functionality /// public static IApplicationBuilder UseUmbracoCore(this IApplicationBuilder app) { From 198586a82b7385d03c7fa902e5d114b9ace4ea84 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Mon, 21 Dec 2020 09:02:05 +0000 Subject: [PATCH 14/14] Dispose host on test teardown. --- .../Testing/UmbracoIntegrationTest.cs | 1 + .../Services/ContentTypeServiceVariantsTests.cs | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 7aea7f37a5..414ad6ec43 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -86,6 +86,7 @@ namespace Umbraco.Tests.Integration.Testing // Ensure CoreRuntime stopped (now it's a HostedService) IHost host = Services.GetRequiredService(); host.StopAsync().GetAwaiter().GetResult(); + host.Dispose(); } [TearDown] diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs index 1fbfb00a1b..995e4d60ae 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs @@ -45,15 +45,6 @@ namespace Umbraco.Tests.Integration.Umbraco.Infrastructure.Services eventBinder.Start(); } - public override void TearDown() - { - // Ensure this is dipsosed - // TODO: How come MSDI doesn't automatically dispose all of these at the end of each test? - // How can we automatically dispose of all IDisposables at the end of the tests as if it were an aspnet app? - Services.GetRequiredService().Dispose(); - base.TearDown(); - } - private void AssertJsonStartsWith(int id, string expected) { var json = GetJson(id).Replace('"', '\'');