diff --git a/src/Umbraco.Core/Cache/GenericDictionaryRequestAppCache.cs b/src/Umbraco.Core/Cache/GenericDictionaryRequestAppCache.cs new file mode 100644 index 0000000000..193235ca7e --- /dev/null +++ b/src/Umbraco.Core/Cache/GenericDictionaryRequestAppCache.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Umbraco.Core.Composing; + +namespace Umbraco.Core.Cache +{ + /// + /// Implements a fast on top of HttpContext.Items. + /// + /// + /// If no current HttpContext items can be found (no current HttpContext, + /// or no Items...) then this cache acts as a pass-through and does not cache + /// anything. + /// + public class GenericDictionaryRequestAppCache : FastDictionaryAppCacheBase, IRequestCache + { + /// + /// Initializes a new instance of the class with a context, for unit tests! + /// + public GenericDictionaryRequestAppCache(Func> requestItems) : base() + { + ContextItems = requestItems; + } + + private Func> ContextItems { get; } + + public bool IsAvailable => TryGetContextItems(out _); + + private bool TryGetContextItems(out IDictionary items) + { + items = ContextItems?.Invoke(); + return items != null; + } + + /// + public override object Get(string key, Func factory) + { + //no place to cache so just return the callback result + if (!TryGetContextItems(out var items)) return factory(); + + key = GetCacheKey(key); + + Lazy result; + + try + { + EnterWriteLock(); + result = items[key] as Lazy; // null if key not found + + // cannot create value within the lock, so if result.IsValueCreated is false, just + // do nothing here - means that if creation throws, a race condition could cause + // more than one thread to reach the return statement below and throw - accepted. + + if (result == null || SafeLazy.GetSafeLazyValue(result, true) == null) // get non-created as NonCreatedValue & exceptions as null + { + result = SafeLazy.GetSafeLazy(factory); + items[key] = result; + } + } + finally + { + ExitWriteLock(); + } + + // using GetSafeLazy and GetSafeLazyValue ensures that we don't cache + // exceptions (but try again and again) and silently eat them - however at + // some point we have to report them - so need to re-throw here + + // this does not throw anymore + //return result.Value; + + var value = result.Value; // will not throw (safe lazy) + if (value is SafeLazy.ExceptionHolder eh) eh.Exception.Throw(); // throw once! + return value; + } + + public bool Set(string key, object value) + { + //no place to cache so just return the callback result + if (!TryGetContextItems(out var items)) return false; + key = GetCacheKey(key); + try + { + + EnterWriteLock(); + items[key] = SafeLazy.GetSafeLazy(() => value); + } + finally + { + ExitWriteLock(); + } + return true; + } + + public bool Remove(string key) + { + //no place to cache so just return the callback result + if (!TryGetContextItems(out var items)) return false; + key = GetCacheKey(key); + try + { + + EnterWriteLock(); + items.Remove(key); + } + finally + { + ExitWriteLock(); + } + return true; + } + + #region Entries + + protected override IEnumerable GetDictionaryEntries() + { + const string prefix = CacheItemPrefix + "-"; + + if (!TryGetContextItems(out var items)) return Enumerable.Empty(); + + return items.Cast() + .Where(x => x.Key is string s && s.StartsWith(prefix)); + } + + protected override void RemoveEntry(string key) + { + if (!TryGetContextItems(out var items)) return; + + items.Remove(key); + } + + protected override object GetEntry(string key) + { + return !TryGetContextItems(out var items) ? null : items[key]; + } + + #endregion + + #region Lock + + private const string ContextItemsLockKey = "Umbraco.Core.Cache.HttpRequestCache::LockEntered"; + + protected override void EnterReadLock() => EnterWriteLock(); + + protected override void EnterWriteLock() + { + if (!TryGetContextItems(out var items)) return; + + // note: cannot keep 'entered' as a class variable here, + // since there is one per request - so storing it within + // ContextItems - which is locked, so this should be safe + + var entered = false; + Monitor.Enter(items, ref entered); + items[ContextItemsLockKey] = entered; + } + + protected override void ExitReadLock() => ExitWriteLock(); + + protected override void ExitWriteLock() + { + if (!TryGetContextItems(out var items)) return; + + var entered = (bool?)items[ContextItemsLockKey] ?? false; + if (entered) + Monitor.Exit(items); + items.Remove(ContextItemsLockKey); + } + + #endregion + + public IEnumerator> GetEnumerator() + { + if (!TryGetContextItems(out var items)) + { + yield break; + } + + foreach (var item in items) + { + yield return new KeyValuePair(item.Key.ToString(), item.Value); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Umbraco.Core/Net/NullSessionIdResolver.cs b/src/Umbraco.Core/Net/NullSessionIdResolver.cs new file mode 100644 index 0000000000..6bfa578268 --- /dev/null +++ b/src/Umbraco.Core/Net/NullSessionIdResolver.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Net +{ + public class NullSessionIdResolver : ISessionIdResolver + { + public string SessionId => null; + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs index 1ea08e3118..19d3716e1c 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs @@ -29,6 +29,7 @@ using Umbraco.Core.Strings; using Umbraco.Core.Sync; using Umbraco.Examine; using Umbraco.Infrastructure.Media; +using Umbraco.Net; using Umbraco.Web; using Umbraco.Web.Actions; using Umbraco.Web.Cache; @@ -351,10 +352,6 @@ namespace Umbraco.Core.Runtime // register accessors for cultures composition.RegisterUnique(); - - - - } } } diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 92e47771a6..ebd91f52a2 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -25,6 +25,7 @@ namespace Umbraco.Core.Runtime private IFactory _factory; private readonly RuntimeState _state; private readonly IUmbracoBootPermissionChecker _umbracoBootPermissionChecker; + private readonly IRequestCache _requestCache; private readonly IGlobalSettings _globalSettings; private readonly IConnectionStrings _connectionStrings; @@ -39,7 +40,8 @@ namespace Umbraco.Core.Runtime IBackOfficeInfo backOfficeInfo, IDbProviderFactoryCreator dbProviderFactoryCreator, IMainDom mainDom, - ITypeFinder typeFinder) + ITypeFinder typeFinder, + IRequestCache requestCache) { IOHelper = ioHelper; Configs = configs; @@ -50,6 +52,7 @@ namespace Umbraco.Core.Runtime DbProviderFactoryCreator = dbProviderFactoryCreator; _umbracoBootPermissionChecker = umbracoBootPermissionChecker; + _requestCache = requestCache; Logger = logger; MainDom = mainDom; @@ -110,6 +113,7 @@ namespace Umbraco.Core.Runtime { if (register is null) throw new ArgumentNullException(nameof(register)); + // create and register the essential services // ie the bare minimum required to boot @@ -129,12 +133,19 @@ namespace Umbraco.Core.Runtime "Booted.", "Boot failed.")) { - Logger.Info("Booting Core"); + + Logger.Info("Booting site '{HostingSiteName}', app '{HostingApplicationId}', path '{HostingPhysicalPath}', server '{MachineName}'.", + HostingEnvironment?.SiteName, + HostingEnvironment?.ApplicationId, + HostingEnvironment?.ApplicationPhysicalPath, + NetworkHelper.MachineName); Logger.Debug("Runtime: {Runtime}", GetType().FullName); // application environment ConfigureUnhandledException(); - return _factory = Configure(register, timer); + _factory = Configure(register, timer); + + return _factory; } } @@ -151,7 +162,7 @@ namespace Umbraco.Core.Runtime try { - + // run handlers RuntimeOptions.DoRuntimeBoot(ProfilingLogger); @@ -244,6 +255,13 @@ namespace Umbraco.Core.Runtime // create & initialize the components _components = _factory.GetInstance(); _components.Initialize(); + + + // now (and only now) is the time to switch over to perWebRequest scopes. + // up until that point we may not have a request, and scoped services would + // fail to resolve - but we run Initialize within a factory scope - and then, + // here, we switch the factory to bind scopes to requests + _factory.EnablePerWebRequestScope(); } protected virtual void ConfigureUnhandledException() @@ -350,7 +368,7 @@ namespace Umbraco.Core.Runtime return new AppCaches( new DeepCloneAppCache(new ObjectCacheAppCache()), - NoAppCache.Instance, + _requestCache, new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache()))); } diff --git a/src/Umbraco.Infrastructure/Runtime/WebRuntime.cs b/src/Umbraco.Infrastructure/Runtime/WebRuntime.cs deleted file mode 100644 index fc2a019023..0000000000 --- a/src/Umbraco.Infrastructure/Runtime/WebRuntime.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.Hosting; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Persistence; -using Umbraco.Core.Runtime; - -namespace Umbraco.Web.Runtime -{ - /// - /// Represents the Web Umbraco runtime. - /// - /// On top of CoreRuntime, handles all of the web-related runtime aspects of Umbraco. - public class WebRuntime : CoreRuntime - { - private readonly IRequestCache _requestCache; - - /// - /// Initializes a new instance of the class. - /// - public WebRuntime( - Configs configs, - IUmbracoVersion umbracoVersion, - IIOHelper ioHelper, - ILogger logger, - IProfiler profiler, - IHostingEnvironment hostingEnvironment, - IBackOfficeInfo backOfficeInfo, - IDbProviderFactoryCreator dbProviderFactoryCreator, - IMainDom mainDom, - ITypeFinder typeFinder, - IRequestCache requestCache, - IUmbracoBootPermissionChecker umbracoBootPermissionChecker): - base(configs, umbracoVersion, ioHelper, logger, profiler ,umbracoBootPermissionChecker, hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, typeFinder) - { - _requestCache = requestCache; - } - - /// - public override IFactory Configure(IRegister register) - { - - var profilingLogger = new ProfilingLogger(Logger, Profiler); - var umbracoVersion = new UmbracoVersion(); - using (var timer = profilingLogger.TraceDuration( - $"Booting Umbraco {umbracoVersion.SemanticVersion.ToSemanticString()}.", - "Booted.", - "Boot failed.")) - { - Logger.Info("Booting site '{HostingSiteName}', app '{HostingApplicationId}', path '{HostingPhysicalPath}', server '{MachineName}'.", - HostingEnvironment.SiteName, - HostingEnvironment.ApplicationId, - HostingEnvironment.ApplicationPhysicalPath, - NetworkHelper.MachineName); - Logger.Debug("Runtime: {Runtime}", GetType().FullName); - - var factory = base.Configure(register); - - // now (and only now) is the time to switch over to perWebRequest scopes. - // up until that point we may not have a request, and scoped services would - // fail to resolve - but we run Initialize within a factory scope - and then, - // here, we switch the factory to bind scopes to requests - factory.EnablePerWebRequestScope(); - - return factory; - } - - - } - - #region Getters - - protected override AppCaches GetAppCaches() => new AppCaches( - // we need to have the dep clone runtime cache provider to ensure - // all entities are cached properly (cloned in and cloned out) - new DeepCloneAppCache(new ObjectCacheAppCache()), - // we need request based cache when running in web-based context - _requestCache, - new IsolatedCaches(type => - // we need to have the dep clone runtime cache provider to ensure - // all entities are cached properly (cloned in and cloned out) - new DeepCloneAppCache(new ObjectCacheAppCache()))); - - #endregion - } -} - diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index 9bc8d31ded..ad040c8ef1 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -4,8 +4,10 @@ using Moq; using NUnit.Framework; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Smidge; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Runtime; @@ -57,7 +59,7 @@ namespace Umbraco.Tests.Integration var coreRuntime = new CoreRuntime(testHelper.GetConfigs(), testHelper.GetUmbracoVersion(), testHelper.IOHelper, testHelper.Logger, testHelper.Profiler, testHelper.UmbracoBootPermissionChecker, testHelper.GetHostingEnvironment(), testHelper.GetBackOfficeInfo(), testHelper.DbProviderFactoryCreator, - testHelper.MainDom, testHelper.GetTypeFinder()); + testHelper.MainDom, testHelper.GetTypeFinder(), NoAppCache.Instance); // boot it! var factory = coreRuntime.Configure(umbracoContainer); @@ -99,7 +101,7 @@ namespace Umbraco.Tests.Integration // Add it! services.AddUmbracoConfiguration(hostContext.Configuration); - services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, testHelper.GetLoggingConfiguration(), out _); + services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, testHelper.GetLoggingConfiguration(), out _); }); var host = await hostBuilder.StartAsync(); @@ -138,7 +140,7 @@ namespace Umbraco.Tests.Integration // Add it! services.AddUmbracoConfiguration(hostContext.Configuration); - services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, testHelper.GetLoggingConfiguration(), out _); + services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, testHelper.GetLoggingConfiguration(), out _); }); var host = await hostBuilder.StartAsync(); diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index f3a2bcf011..0137850408 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -108,7 +108,7 @@ namespace Umbraco.Tests.Integration.Testing // Add it! services.AddUmbracoConfiguration(hostContext.Configuration); - services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, testHelper.GetLoggingConfiguration(), out _); + services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly, NoAppCache.Instance, testHelper.GetLoggingConfiguration(), out _); }); var host = await hostBuilder.StartAsync(); diff --git a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs index b900453a5e..960d355c0d 100644 --- a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs +++ b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs @@ -16,10 +16,10 @@ using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using Umbraco.Core.Strings; using Umbraco.Core.Configuration; -using Umbraco.Core.Dictionary; using Umbraco.Core.Hosting; using Umbraco.Core.IO; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Runtime; using Umbraco.Core.Services; using Umbraco.Tests.PublishedContent; using Umbraco.Tests.Testing; @@ -47,10 +47,10 @@ namespace Umbraco.Tests.Routing HostingEnvironment); } - public class TestRuntime : WebRuntime + public class TestRuntime : CoreRuntime { public TestRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo) - : base(configs, umbracoVersion, ioHelper, Mock.Of(), Mock.Of(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), TestHelper.GetRequestCache(), new AspNetUmbracoBootPermissionChecker()) + : base(configs, umbracoVersion, ioHelper, Mock.Of(), Mock.Of(), new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), NoAppCache.Instance) { } diff --git a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs index 488a4f6dad..0b90457b1e 100644 --- a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs +++ b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs @@ -5,6 +5,7 @@ using Examine; using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; @@ -116,7 +117,7 @@ namespace Umbraco.Tests.Runtimes public class TestRuntime : CoreRuntime { public TestRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, IProfiler profiler, IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo) - :base(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder()) + :base(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), NoAppCache.Instance) { } diff --git a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs index 1a4c7f2040..5100e2e21c 100644 --- a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs +++ b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs @@ -76,7 +76,7 @@ namespace Umbraco.Tests.Runtimes var runtimeState = new RuntimeState(logger, null, umbracoVersion, backOfficeInfo); var configs = TestHelper.GetConfigs(); var variationContextAccessor = TestHelper.VariationContextAccessor; - + // create the register and the composition var register = TestHelper.GetRegister(); @@ -84,7 +84,7 @@ namespace Umbraco.Tests.Runtimes composition.RegisterEssentials(logger, profiler, profilingLogger, mainDom, appCaches, databaseFactory, typeLoader, runtimeState, typeFinder, ioHelper, umbracoVersion, TestHelper.DbProviderFactoryCreator, hostingEnvironment, backOfficeInfo); // create the core runtime and have it compose itself - var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder); + var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder, NoAppCache.Instance); // determine actual runtime level runtimeState.DetermineRuntimeLevel(databaseFactory, logger); @@ -278,7 +278,7 @@ namespace Umbraco.Tests.Runtimes composition.RegisterEssentials(logger, profiler, profilingLogger, mainDom, appCaches, databaseFactory, typeLoader, runtimeState, typeFinder, ioHelper, umbracoVersion, TestHelper.DbProviderFactoryCreator, hostingEnvironment, backOfficeInfo); // create the core runtime and have it compose itself - var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder); + var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, typeFinder, NoAppCache.Instance); // get the components // all of them? @@ -322,8 +322,8 @@ namespace Umbraco.Tests.Runtimes Assert.AreEqual(0, results.Count); } - - + + } } diff --git a/src/Umbraco.Web.BackOffice/Filters/MinifyJavaScriptResultAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/MinifyJavaScriptResultAttribute.cs index 0ed0fd658a..65c2be051f 100644 --- a/src/Umbraco.Web.BackOffice/Filters/MinifyJavaScriptResultAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/MinifyJavaScriptResultAttribute.cs @@ -2,9 +2,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Umbraco.Core.Hosting; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core.Runtime; using Umbraco.Core.WebAssets; -using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.Common.ActionResults; namespace Umbraco.Web.BackOffice.Filters @@ -14,10 +12,11 @@ namespace Umbraco.Web.BackOffice.Filters public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { // logic before action goes here - var hostingEnvironment = context.HttpContext.RequestServices.GetService(); + var serviceProvider = context.HttpContext.RequestServices; + var hostingEnvironment = serviceProvider.GetService(); if (!hostingEnvironment.IsDebugMode) { - var runtimeMinifier = context.HttpContext.RequestServices.GetService(); + var runtimeMinifier = serviceProvider.GetService(); if (context.Result is JavaScriptResult jsResult) { diff --git a/src/Umbraco.Web.Common/Constants/ViewConstants.cs b/src/Umbraco.Web.Common/Constants/ViewConstants.cs new file mode 100644 index 0000000000..5da1a74f55 --- /dev/null +++ b/src/Umbraco.Web.Common/Constants/ViewConstants.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Web.Common.Constants +{ + /// + /// constants + /// + internal static class ViewConstants + { + internal const string ViewLocation = "~/Views"; + + internal const string DataTokenCurrentViewContext = "umbraco-current-view-context"; + } +} diff --git a/src/Umbraco.Web.Common/Controllers/RenderController.cs b/src/Umbraco.Web.Common/Controllers/RenderController.cs new file mode 100644 index 0000000000..43058616de --- /dev/null +++ b/src/Umbraco.Web.Common/Controllers/RenderController.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Umbraco.Web.Common.Controllers +{ + public abstract class RenderController : Controller + { + + } +} diff --git a/src/Umbraco.Web.Common/Events/ActionExecutedEventArgs.cs b/src/Umbraco.Web.Common/Events/ActionExecutedEventArgs.cs new file mode 100644 index 0000000000..b33cbc7d8a --- /dev/null +++ b/src/Umbraco.Web.Common/Events/ActionExecutedEventArgs.cs @@ -0,0 +1,17 @@ +using System; +using Microsoft.AspNetCore.Mvc; + +namespace Umbraco.Web.Common.Events +{ + public class ActionExecutedEventArgs : EventArgs + { + public Controller Controller { get; set; } + public object Model { get; set; } + + public ActionExecutedEventArgs(Controller controller, object model) + { + Controller = controller; + Model = model; + } + } +} diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index 2439243f30..3facf1b77f 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Data.Common; using System.IO; using System.Reflection; @@ -80,7 +82,17 @@ namespace Umbraco.Web.Common.Extensions Path.Combine(webHostEnvironment.ContentRootPath, "config\\serilog.config"), Path.Combine(webHostEnvironment.ContentRootPath, "config\\serilog.user.config")); - services.AddUmbracoCore(webHostEnvironment, umbContainer, Assembly.GetEntryAssembly(), loggingConfig, out factory); + IHttpContextAccessor httpContextAccessor = new HttpContextAccessor(); + services.AddSingleton(httpContextAccessor); + + var requestCache = new GenericDictionaryRequestAppCache(() => httpContextAccessor.HttpContext.Items); + + services.AddUmbracoCore(webHostEnvironment, + umbContainer, + Assembly.GetEntryAssembly(), + requestCache, + loggingConfig, + out factory); return services; } @@ -92,6 +104,8 @@ namespace Umbraco.Web.Common.Extensions /// /// /// + /// + /// /// /// /// @@ -100,6 +114,7 @@ namespace Umbraco.Web.Common.Extensions IWebHostEnvironment webHostEnvironment, IRegister umbContainer, Assembly entryAssembly, + IRequestCache requestCache, ILoggingConfiguration loggingConfiguration, out IFactory factory) { @@ -108,11 +123,15 @@ namespace Umbraco.Web.Common.Extensions if (container is null) throw new ArgumentNullException(nameof(container)); if (entryAssembly is null) throw new ArgumentNullException(nameof(entryAssembly)); - // Special case! The generic host adds a few default services but we need to manually add this one here NOW because - // we resolve it before the host finishes configuring in the call to CreateCompositionRoot - services.AddSingleton(); + var serviceProvider = services.BuildServiceProvider(); + var configs = serviceProvider.GetService(); - CreateCompositionRoot(services, webHostEnvironment, loggingConfiguration, out var logger, out var configs, out var ioHelper, out var hostingEnvironment, out var backOfficeInfo, out var profiler); + + CreateCompositionRoot(services, + configs, + webHostEnvironment, + loggingConfiguration, + out var logger, out var ioHelper, out var hostingEnvironment, out var backOfficeInfo, out var profiler); var globalSettings = configs.Global(); var umbracoVersion = new UmbracoVersion(globalSettings); @@ -125,7 +144,8 @@ namespace Umbraco.Web.Common.Extensions profiler, hostingEnvironment, backOfficeInfo, - CreateTypeFinder(logger, profiler, webHostEnvironment, entryAssembly)); + CreateTypeFinder(logger, profiler, webHostEnvironment, entryAssembly), + requestCache); factory = coreRuntime.Configure(container); @@ -142,11 +162,12 @@ namespace Umbraco.Web.Common.Extensions return new TypeFinder(logger, new DefaultUmbracoAssemblyProvider(entryAssembly), runtimeHash); } - private static IRuntime GetCoreRuntime(Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, Core.Logging.ILogger logger, + private static IRuntime GetCoreRuntime( + Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, Core.Logging.ILogger logger, IProfiler profiler, Core.Hosting.IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo, - ITypeFinder typeFinder) + ITypeFinder typeFinder, IRequestCache requestCache) { - var connectionStringConfig = configs.ConnectionStrings()[Constants.System.UmbracoConnectionName]; + var connectionStringConfig = configs.ConnectionStrings()[Core.Constants.System.UmbracoConnectionName]; var dbProviderFactoryCreator = new SqlServerDbProviderFactoryCreator( connectionStringConfig?.ProviderName, DbProviderFactories.GetFactory); @@ -161,23 +182,34 @@ namespace Umbraco.Web.Common.Extensions var mainDom = new MainDom(logger, mainDomLock); - var coreRuntime = new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, new AspNetCoreBootPermissionsChecker(), - hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, typeFinder); + var coreRuntime = new CoreRuntime( + configs, + umbracoVersion, + ioHelper, + logger, + profiler, + new AspNetCoreBootPermissionsChecker(), + hostingEnvironment, + backOfficeInfo, + dbProviderFactoryCreator, + mainDom, + typeFinder, + requestCache); return coreRuntime; } private static IServiceCollection CreateCompositionRoot( IServiceCollection services, + Configs configs, IWebHostEnvironment webHostEnvironment, - ILoggingConfiguration loggingConfiguration, - out Core.Logging.ILogger logger, out Configs configs, out IIOHelper ioHelper, out Core.Hosting.IHostingEnvironment hostingEnvironment, - out IBackOfficeInfo backOfficeInfo, out IProfiler profiler) + ILoggingConfiguration loggingConfiguration, + out Core.Logging.ILogger logger, + out IIOHelper ioHelper, + out Core.Hosting.IHostingEnvironment hostingEnvironment, + out IBackOfficeInfo backOfficeInfo, + out IProfiler profiler) { - // TODO: We need to avoid this, surely there's a way? See ContainerTests.BuildServiceProvider_Before_Host_Is_Configured - var serviceProvider = services.BuildServiceProvider(); - - configs = serviceProvider.GetService(); if (configs == null) throw new InvalidOperationException($"Could not resolve type {typeof(Configs)} from the container, ensure {nameof(AddUmbracoConfiguration)} is called before calling {nameof(AddUmbracoCore)}"); @@ -205,13 +237,13 @@ namespace Umbraco.Web.Common.Extensions // Wire up all the bits that serilog needs. We need to use our own code since the Serilog ext methods don't cater to our needs since // we don't want to use the global serilog `Log` object and we don't have our own ILogger implementation before the HostBuilder runs which - // is the only other option that these ext methods allow. + // is the only other option that these ext methods allow. // I have created a PR to make this nicer https://github.com/serilog/serilog-extensions-hosting/pull/19 but we'll need to wait for that. // Also see : https://github.com/serilog/serilog-extensions-hosting/blob/dev/src/Serilog.Extensions.Hosting/SerilogHostBuilderExtensions.cs services.AddSingleton(services => new SerilogLoggerFactory(logger.SerilogLog, false)); - // This won't (and shouldn't) take ownership of the logger. + // This won't (and shouldn't) take ownership of the logger. services.AddSingleton(logger.SerilogLog); // Registered to provide two services... @@ -229,7 +261,7 @@ namespace Umbraco.Web.Common.Extensions public static IServiceCollection AddUmbracoRuntimeMinifier(this IServiceCollection services, IConfiguration configuration) { - services.AddSmidge(configuration.GetSection(Constants.Configuration.ConfigRuntimeMinification)); + services.AddSmidge(configuration.GetSection(Core.Constants.Configuration.ConfigRuntimeMinification)); services.AddSmidgeNuglify(); return services; @@ -260,4 +292,5 @@ namespace Umbraco.Web.Common.Extensions } + } diff --git a/src/Umbraco.Web.Common/Filters/DisableBrowserCacheAttribute.cs b/src/Umbraco.Web.Common/Filters/DisableBrowserCacheAttribute.cs new file mode 100644 index 0000000000..0fe251bac4 --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/DisableBrowserCacheAttribute.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Net.Http.Headers; + +namespace Umbraco.Web.Common.Filters +{ + /// + /// Ensures that the request is not cached by the browser + /// + public class DisableBrowserCacheAttribute : ActionFilterAttribute + { + public override void OnResultExecuting(ResultExecutingContext context) + { + base.OnResultExecuting(context); + + var httpResponse = context.HttpContext.Response; + + if (httpResponse.StatusCode != 200) return; + + httpResponse.GetTypedHeaders().CacheControl = + new CacheControlHeaderValue() + { + NoCache = true, + MaxAge = TimeSpan.Zero, + MustRevalidate = true, + NoStore = true + }; + + httpResponse.Headers[HeaderNames.LastModified] = DateTime.Now.ToString("R"); // Format RFC1123 + httpResponse.Headers[HeaderNames.Pragma] = "no-cache"; + httpResponse.Headers[HeaderNames.Expires] = new DateTime(1990, 1, 1, 0, 0, 0).ToString("R"); + } + } +} diff --git a/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs new file mode 100644 index 0000000000..269e437d0d --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs @@ -0,0 +1,88 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Umbraco.Web.Common.Constants; +using Umbraco.Web.Common.Controllers; + +namespace Umbraco.Web.Common.Filters +{ + /// + /// This is a special filter which is required for the RTE to be able to render Partial View Macros that + /// contain forms when the RTE value is resolved outside of an MVC view being rendered + /// + /// + /// The entire way that we support partial view macros that contain forms isn't really great, these forms + /// need to be executed as ChildActions so that the ModelState,ViewData,TempData get merged into that action + /// so the form can show errors, viewdata, etc... + /// Under normal circumstances, macros will be rendered after a ViewContext is created but in some cases + /// developers will resolve the RTE value in the controller, in this case the Form won't be rendered correctly + /// with merged ModelState from the controller because the special DataToken hasn't been set yet (which is + /// normally done in the UmbracoViewPageOfModel when a real ViewContext is available. + /// So we need to detect if the currently rendering controller is IRenderController and if so we'll ensure that + /// this DataToken exists before the action executes in case the developer resolves an RTE value that contains + /// a partial view macro form. + /// + public class EnsurePartialViewMacroViewContextFilterAttribute : ActionFilterAttribute + { + + /// + /// Ensures the custom ViewContext datatoken is set before the RenderController action is invoked, + /// 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; + + //ignore anything that is not IRenderController + if (!(controller is RenderController)) return; + + SetViewContext(context, controller); + } + + /// + /// Ensures that the custom ViewContext datatoken is set after the RenderController action is invoked, + /// this ensures that any custom ModelState that may have been added in the RenderController itself is + /// passed onwards in case it is required when rendering a PartialViewMacro with a form + /// + /// The filter context. + public override void OnResultExecuting(ResultExecutingContext context) + { + if (!(context.Controller is Controller controller)) return; + + //ignore anything that is not IRenderController + if (!(controller is RenderController)) return; + + SetViewContext(context, controller); + } + + private void SetViewContext(ActionContext context, Controller controller) + { + var viewCtx = new ViewContext( + context, + new DummyView(), + controller.ViewData, + controller.TempData, + new StringWriter(), + new HtmlHelperOptions()); + + //set the special data token + context.RouteData.DataTokens[ViewConstants.DataTokenCurrentViewContext] = viewCtx; + } + + private class DummyView : IView + { + public Task RenderAsync(ViewContext context) + { + return Task.CompletedTask; + } + + public string Path { get; } + } + } +} diff --git a/src/Umbraco.Web.Common/Filters/PreRenderViewActionFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/PreRenderViewActionFilterAttribute.cs new file mode 100644 index 0000000000..2ba58a8fd7 --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/PreRenderViewActionFilterAttribute.cs @@ -0,0 +1,43 @@ +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/Filters/StatusCodeResultAttribute.cs b/src/Umbraco.Web.Common/Filters/StatusCodeResultAttribute.cs new file mode 100644 index 0000000000..8f3fcf3a95 --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/StatusCodeResultAttribute.cs @@ -0,0 +1,39 @@ +using System.Net; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core.Configuration.UmbracoSettings; + +namespace Umbraco.Web.Common.Filters +{ + /// + /// Forces the response to have a specific http status code + /// + public class StatusCodeResultAttribute : ActionFilterAttribute + { + private readonly HttpStatusCode _statusCode; + + public StatusCodeResultAttribute(HttpStatusCode statusCode) + { + _statusCode = statusCode; + } + + public override void OnActionExecuted(ActionExecutedContext context) + { + base.OnActionExecuted(context); + + var httpContext = context.HttpContext; + + httpContext.Response.StatusCode = (int)_statusCode; + + var disableIisCustomErrors = httpContext.RequestServices.GetService().TrySkipIisCustomErrors; + var statusCodePagesFeature = httpContext.Features.Get(); + + if (statusCodePagesFeature != null) + { + // if IIS Custom Errors are disabled, we won't enable the Status Code Pages + statusCodePagesFeature.Enabled = !disableIisCustomErrors; + } + } + } +} diff --git a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs index ba3c6c289f..c4b9522099 100644 --- a/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs +++ b/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs @@ -112,7 +112,7 @@ namespace Umbraco.Web.Common.RuntimeMinification public void Reset() { var version = DateTime.UtcNow.Ticks.ToString(); - _configManipulator.SaveConfigValue(Constants.Configuration.ConfigRuntimeMinificationVersion, version.ToString()); + _configManipulator.SaveConfigValue(Core.Constants.Configuration.ConfigRuntimeMinificationVersion, version.ToString()); } diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 8602d5bed6..37440006aa 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -76,6 +76,7 @@ namespace Umbraco.Web.UI.BackOffice { app.UseDeveloperExceptionPage(); } + app.UseStatusCodePages(); app.UseUmbracoCore(); app.UseUmbracoRequestLogging(); app.UseUmbracoWebsite(); diff --git a/src/Umbraco.Web/Mvc/ActionExecutedEventArgs.cs b/src/Umbraco.Web/Mvc/ActionExecutedEventArgs.cs index 1c596ff80c..6904aa103a 100644 --- a/src/Umbraco.Web/Mvc/ActionExecutedEventArgs.cs +++ b/src/Umbraco.Web/Mvc/ActionExecutedEventArgs.cs @@ -3,6 +3,7 @@ using System.Web.Mvc; namespace Umbraco.Web.Mvc { + /// Migrated already to .Net Core public class ActionExecutedEventArgs : EventArgs { public Controller Controller { get; set; } diff --git a/src/Umbraco.Web/Mvc/Constants.cs b/src/Umbraco.Web/Mvc/Constants.cs index 1794345746..c71ed6b104 100644 --- a/src/Umbraco.Web/Mvc/Constants.cs +++ b/src/Umbraco.Web/Mvc/Constants.cs @@ -3,6 +3,7 @@ /// /// constants /// + /// Migrated already to .Net Core internal static class Constants { internal const string ViewLocation = "~/Views"; diff --git a/src/Umbraco.Web/Mvc/EnsurePartialViewMacroViewContextFilterAttribute.cs b/src/Umbraco.Web/Mvc/EnsurePartialViewMacroViewContextFilterAttribute.cs index 34b857dfb4..f443abbb70 100644 --- a/src/Umbraco.Web/Mvc/EnsurePartialViewMacroViewContextFilterAttribute.cs +++ b/src/Umbraco.Web/Mvc/EnsurePartialViewMacroViewContextFilterAttribute.cs @@ -19,6 +19,7 @@ namespace Umbraco.Web.Mvc /// this DataToken exists before the action executes in case the developer resolves an RTE value that contains /// a partial view macro form. /// + /// Migrated already to .Net Core internal class EnsurePartialViewMacroViewContextFilterAttribute : ActionFilterAttribute { /// diff --git a/src/Umbraco.Web/Mvc/IRenderController.cs b/src/Umbraco.Web/Mvc/IRenderController.cs index 103a484869..0de585959c 100644 --- a/src/Umbraco.Web/Mvc/IRenderController.cs +++ b/src/Umbraco.Web/Mvc/IRenderController.cs @@ -5,6 +5,7 @@ namespace Umbraco.Web.Mvc /// /// A marker interface to designate that a controller will be used for Umbraco front-end requests and/or route hijacking /// + /// Migrated already to .Net Core public interface IRenderController : IController { diff --git a/src/Umbraco.Web/Mvc/MinifyJavaScriptResultAttribute.cs b/src/Umbraco.Web/Mvc/MinifyJavaScriptResultAttribute.cs index 227c15b344..635a7314c5 100644 --- a/src/Umbraco.Web/Mvc/MinifyJavaScriptResultAttribute.cs +++ b/src/Umbraco.Web/Mvc/MinifyJavaScriptResultAttribute.cs @@ -12,6 +12,7 @@ namespace Umbraco.Web.Mvc /// /// Only minifies in release mode /// + /// Migrated already to .Net Core public class MinifyJavaScriptResultAttribute : ActionFilterAttribute { private readonly IHostingEnvironment _hostingEnvironment; diff --git a/src/Umbraco.Web/Mvc/PreRenderViewActionFilterAttribute.cs b/src/Umbraco.Web/Mvc/PreRenderViewActionFilterAttribute.cs index 54e20f5d56..2e659eccf6 100644 --- a/src/Umbraco.Web/Mvc/PreRenderViewActionFilterAttribute.cs +++ b/src/Umbraco.Web/Mvc/PreRenderViewActionFilterAttribute.cs @@ -3,6 +3,7 @@ using System.Web.Mvc; namespace Umbraco.Web.Mvc { + /// Migrated already to .Net Core public class PreRenderViewActionFilterAttribute : ActionFilterAttribute { public override void OnActionExecuted(ActionExecutedContext filterContext) diff --git a/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs b/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs index b1836c6ba4..727c29b93c 100644 --- a/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs +++ b/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs @@ -8,6 +8,7 @@ namespace Umbraco.Web.Mvc /// /// Forces the response to have a specific http status code /// + /// Migrated already to .Net Core internal class StatusCodeResultAttribute : ActionFilterAttribute { private readonly HttpStatusCode _statusCode; diff --git a/src/Umbraco.Web/UmbracoApplication.cs b/src/Umbraco.Web/UmbracoApplication.cs index cd72b2faf9..7679da2e2e 100644 --- a/src/Umbraco.Web/UmbracoApplication.cs +++ b/src/Umbraco.Web/UmbracoApplication.cs @@ -1,4 +1,7 @@ -using System.Threading; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Web; using Umbraco.Core; using Umbraco.Core.Cache; @@ -34,10 +37,12 @@ namespace Umbraco.Web var mainDom = new MainDom(logger, mainDomLock); - var requestCache = new HttpRequestAppCache(() => HttpContext.Current?.Items); + var requestCache = new HttpRequestAppCache(() => HttpContext.Current != null ? HttpContext.Current.Items : null); var umbracoBootPermissionChecker = new AspNetUmbracoBootPermissionChecker(); - return new WebRuntime(configs, umbracoVersion, ioHelper, logger, profiler, hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, - GetTypeFinder(hostingEnvironment, logger, profiler), requestCache, umbracoBootPermissionChecker); + return new CoreRuntime(configs, umbracoVersion, ioHelper, logger, profiler, umbracoBootPermissionChecker, hostingEnvironment, backOfficeInfo, dbProviderFactoryCreator, mainDom, + GetTypeFinder(hostingEnvironment, logger, profiler), requestCache); } + + } }