From 10bc7d0c1ee1f96dc4726979251be2c8f3d5fdf1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 12 Nov 2014 18:32:51 +1100 Subject: [PATCH] Makes the section scanning and loading from the xml lazy so this no longer happens on startup but instead when sections are required, this ensures during startup there is no IO or scanning for sections, this should assist with U4-5126 config files get cleared out with zero size and of course also help a bunch with startup time. --- .../Services/ApplicationTreeService.cs | 8 +- src/Umbraco.Core/Services/ISectionService.cs | 11 +- src/Umbraco.Core/Services/SectionService.cs | 143 +++++++++++++----- src/Umbraco.Core/Services/ServiceContext.cs | 2 +- .../ApplicationRegistrar.cs | 72 +++++++-- 5 files changed, 179 insertions(+), 57 deletions(-) diff --git a/src/Umbraco.Core/Services/ApplicationTreeService.cs b/src/Umbraco.Core/Services/ApplicationTreeService.cs index ca6bb122d1..5278990d6a 100644 --- a/src/Umbraco.Core/Services/ApplicationTreeService.cs +++ b/src/Umbraco.Core/Services/ApplicationTreeService.cs @@ -18,16 +18,16 @@ namespace Umbraco.Core.Services private readonly CacheHelper _cache; private IEnumerable _allAvailableTrees; private volatile bool _isInitialized = false; - private readonly object _locker = new object(); + internal const string TreeConfigFileName = "trees.config"; + private static string _treeConfig; + private static readonly object Locker = new object(); public ApplicationTreeService(CacheHelper cache) { _cache = cache; } - internal const string TreeConfigFileName = "trees.config"; - private static string _treeConfig; - private static readonly object Locker = new object(); + /// /// gets/sets the trees.config file path diff --git a/src/Umbraco.Core/Services/ISectionService.cs b/src/Umbraco.Core/Services/ISectionService.cs index bc950b9470..df5c89d4bf 100644 --- a/src/Umbraco.Core/Services/ISectionService.cs +++ b/src/Umbraco.Core/Services/ISectionService.cs @@ -6,10 +6,15 @@ namespace Umbraco.Core.Services public interface ISectionService { /// - /// Ensures all available sections exist in the storage medium + /// Initializes the service with all available application plugins /// - /// - void Initialize(IEnumerable
existingSections); + /// + /// All application plugins found in assemblies + /// + /// + /// This is used to populate the app.config file with any applications declared in plugins that don't exist in the file + /// + void Initialize(IEnumerable
allAvailableSections); /// /// The cache storage for all applications diff --git a/src/Umbraco.Core/Services/SectionService.cs b/src/Umbraco.Core/Services/SectionService.cs index 00fa28e38c..1a3568cff3 100644 --- a/src/Umbraco.Core/Services/SectionService.cs +++ b/src/Umbraco.Core/Services/SectionService.cs @@ -17,12 +17,19 @@ namespace Umbraco.Core.Services internal class SectionService : ISectionService { private readonly IUserService _userService; + private IEnumerable
_allAvailableSections; private readonly IApplicationTreeService _applicationTreeService; + private readonly IDatabaseUnitOfWorkProvider _uowProvider; private readonly CacheHelper _cache; + internal const string AppConfigFileName = "applications.config"; + private static string _appConfig; + private volatile bool _isInitialized = false; + private static readonly object Locker = new object(); public SectionService( IUserService userService, - IApplicationTreeService applicationTreeService, + IApplicationTreeService applicationTreeService, + IDatabaseUnitOfWorkProvider uowProvider, CacheHelper cache) { if (applicationTreeService == null) throw new ArgumentNullException("applicationTreeService"); @@ -30,12 +37,11 @@ namespace Umbraco.Core.Services _userService = userService; _applicationTreeService = applicationTreeService; + _uowProvider = uowProvider; _cache = cache; } - internal const string AppConfigFileName = "applications.config"; - private static string _appConfig; - private static readonly object Locker = new object(); + /// /// gets/sets the application.config file path @@ -57,22 +63,17 @@ namespace Umbraco.Core.Services } /// - /// Ensures all available sections exist in the storage medium + /// Initializes the service with all available application plugins /// - /// - public void Initialize(IEnumerable
existingSections) + /// + /// All application plugins found in assemblies + /// + /// + /// This is used to populate the app.config file with any applications declared in plugins that don't exist in the file + /// + public void Initialize(IEnumerable
allAvailableSections) { - LoadXml(doc => - { - foreach (var attr in existingSections) - { - doc.Root.Add(new XElement("add", - new XAttribute("alias", attr.Alias), - new XAttribute("name", attr.Name), - new XAttribute("icon", attr.Icon), - new XAttribute("sortOrder", attr.SortOrder))); - } - }, true); + _allAvailableSections = allAvailableSections; } /// @@ -80,7 +81,7 @@ namespace Umbraco.Core.Services /// public IEnumerable
GetSections() { - return _cache.GetCacheItem( + return _cache.RuntimeCache.GetCacheItem>( CacheKeys.ApplicationsCacheKey, () => { @@ -88,28 +89,66 @@ namespace Umbraco.Core.Services //if (_testApps != null) // return _testApps; - var tmp = new List
(); + var list = ReadFromXmlAndSort(); - LoadXml(doc => + //On first access we need to do some initialization + if (_isInitialized == false) + { + lock (Locker) { - foreach (var addElement in doc.Root.Elements("add").OrderBy(x => - { - var sortOrderAttr = x.Attribute("sortOrder"); - return sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0; - })) + if (_isInitialized == false) { - var sortOrderAttr = addElement.Attribute("sortOrder"); - tmp.Add(new Section(addElement.Attribute("name").Value, - addElement.Attribute("alias").Value, - addElement.Attribute("icon").Value, - sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0)); + //now we can check the non-volatile flag + if (_allAvailableSections != null) + { + var hasChanges = false; + + LoadXml(doc => + { + //Now, load in the xml structure and update it with anything that is not declared there and save the file. + + //NOTE: On the first iteration here, it will lazily scan all apps, etc... this is because this ienumerable is lazy + // based on the ApplicationRegistrar - and as noted there this is not an ideal way to do things but were stuck like this + // currently because of the legacy assemblies and types not in the Core. + + //Get all the trees not registered in the config + var unregistered = _allAvailableSections + .Where(x => list.Any(l => l.Alias == x.Alias) == false) + .ToArray(); + + hasChanges = unregistered.Any(); + + var count = 0; + foreach (var attr in unregistered) + { + doc.Root.Add(new XElement("add", + new XAttribute("alias", attr.Alias), + new XAttribute("name", attr.Name), + new XAttribute("icon", attr.Icon), + new XAttribute("sortOrder", attr.SortOrder))); + count++; + } + + //don't save if there's no changes + return count > 0; + }, true); + + if (hasChanges) + { + //If there were changes, we need to re-read the structures from the XML + list = ReadFromXmlAndSort(); + } + } } - }, false); - return tmp; + } + } + + return list; + }); } - internal void LoadXml(Action callback, bool saveAfterCallback) + internal void LoadXml(Func callback, bool saveAfterCallbackIfChanged) { lock (Locker) { @@ -119,9 +158,9 @@ namespace Umbraco.Core.Services if (doc.Root != null) { - callback.Invoke(doc); + var changed = callback.Invoke(doc); - if (saveAfterCallback) + if (saveAfterCallbackIfChanged && changed) { //ensure the folder is created! Directory.CreateDirectory(Path.GetDirectoryName(AppConfigFilePath)); @@ -194,6 +233,7 @@ namespace Umbraco.Core.Services new XAttribute("name", name), new XAttribute("icon", icon), new XAttribute("sortOrder", sortOrder))); + return true; }, true); //raise event @@ -209,7 +249,7 @@ namespace Umbraco.Core.Services lock (Locker) { //delete the assigned applications - ApplicationContext.Current.DatabaseContext.Database.Execute( + _uowProvider.GetUnitOfWork().Database.Execute( "delete from umbracoUser2App where app = @appAlias", new { appAlias = section.Alias }); @@ -222,7 +262,10 @@ namespace Umbraco.Core.Services LoadXml(doc => { - doc.Root.Elements("add").Where(x => x.Attribute("alias") != null && x.Attribute("alias").Value == section.Alias).Remove(); + doc.Root.Elements("add").Where(x => x.Attribute("alias") != null && x.Attribute("alias").Value == section.Alias) + .Remove(); + + return true; }, true); //raise event @@ -230,6 +273,30 @@ namespace Umbraco.Core.Services } } + private List
ReadFromXmlAndSort() + { + var tmp = new List
(); + + LoadXml(doc => + { + foreach (var addElement in doc.Root.Elements("add").OrderBy(x => + { + var sortOrderAttr = x.Attribute("sortOrder"); + return sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0; + })) + { + var sortOrderAttr = addElement.Attribute("sortOrder"); + tmp.Add(new Section(addElement.Attribute("name").Value, + addElement.Attribute("alias").Value, + addElement.Attribute("icon").Value, + sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0)); + } + return false; + }, false); + + return tmp; + } + internal static event TypedEventHandler Deleted; private static void OnDeleted(Section app, EventArgs args) { diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index 375e1ca1ba..a6e74b00d0 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -162,7 +162,7 @@ namespace Umbraco.Core.Services _treeService = new Lazy(() => new ApplicationTreeService(cache)); if (_sectionService == null) - _sectionService = new Lazy(() => new SectionService(_userService.Value, _treeService.Value, cache)); + _sectionService = new Lazy(() => new SectionService(_userService.Value, _treeService.Value, provider, cache)); if (_macroService == null) _macroService = new Lazy(() => new MacroService(provider, repositoryFactory.Value)); diff --git a/src/umbraco.businesslogic/ApplicationRegistrar.cs b/src/umbraco.businesslogic/ApplicationRegistrar.cs index dbbf3249cf..4c284653d8 100644 --- a/src/umbraco.businesslogic/ApplicationRegistrar.cs +++ b/src/umbraco.businesslogic/ApplicationRegistrar.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Configuration; using System.Data.SqlClient; using System.Linq; @@ -16,23 +18,26 @@ using umbraco.interfaces; namespace umbraco.BusinessLogic { + /// + /// A startup handler for putting the app config in the config file based on attributes found + /// + /// /// + /// TODO: This is really not a very ideal process but the code is found here because tree plugins are in the Web project or the legacy business logic project. + /// Moving forward we can put the base tree plugin classes in the core and then this can all just be taken care of normally within the service. + /// public class ApplicationRegistrar : ApplicationEventHandler, IMapperConfiguration { protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { - // Load all Applications by attribute and add them to the XML config - var types = PluginManager.Current.ResolveApplications(); - - //since applications don't populate their metadata from the attribute and because it is an interface, - //we need to interrogate the attributes for the data. Would be better to have a base class that contains - //metadata populated by the attribute. Oh well i guess. - var attrs = types.Select(x => x.GetCustomAttributes(false).Single()) - .Where(x => applicationContext.Services.SectionService.GetByAlias(x.Alias) == null) - .ToArray(); - - applicationContext.Services.SectionService.Initialize(attrs.Select(x => new Section(x.Name, x.Alias, x.Icon, x.SortOrder))); + //initialize the section service with a lazy collection of found app plugins + applicationContext.Services.SectionService.Initialize(new LazyEnumerableSections()); } + /// + /// Configure a few auto-mappings + /// + /// + /// public void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { config.CreateMap() @@ -46,5 +51,50 @@ namespace umbraco.BusinessLogic .ForMember(x => x.Name, expression => expression.MapFrom(x => x.name)) .ForMember(x => x.SortOrder, expression => expression.MapFrom(x => x.sortOrder)); } + + /// + /// This class is here so that we can provide lazy access to tree scanning for when it is needed + /// + private class LazyEnumerableSections : IEnumerable
+ { + public LazyEnumerableSections() + { + _lazySections = new Lazy>(() => + { + // Load all Applications by attribute and add them to the XML config + var types = PluginManager.Current.ResolveApplications(); + + //since applications don't populate their metadata from the attribute and because it is an interface, + //we need to interrogate the attributes for the data. Would be better to have a base class that contains + //metadata populated by the attribute. Oh well i guess. + var attrs = types.Select(x => x.GetCustomAttributes(false).Single()); + return attrs.Select(x => new Section(x.Name, x.Alias, x.Icon, x.SortOrder)).ToArray(); + }); + } + + private readonly Lazy> _lazySections; + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + public IEnumerator
GetEnumerator() + { + return _lazySections.Value.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } } } \ No newline at end of file