diff --git a/src/Umbraco.Core/ApplicationEventHandler.cs b/src/Umbraco.Core/ApplicationEventHandler.cs index 8d97baac95..a725a08a8e 100644 --- a/src/Umbraco.Core/ApplicationEventHandler.cs +++ b/src/Umbraco.Core/ApplicationEventHandler.cs @@ -74,7 +74,7 @@ namespace Umbraco.Core /// private bool ShouldExecute(ApplicationContext applicationContext) { - if (applicationContext.IsConfigured && applicationContext.DatabaseContext.IsDatabaseConfigured) + if (applicationContext.IsConfigured && applicationContext.DatabaseContext.CanConnect) { return true; } @@ -84,7 +84,7 @@ namespace Umbraco.Core return true; } - if (!applicationContext.DatabaseContext.IsDatabaseConfigured && ExecuteWhenDatabaseNotConfigured) + if (!applicationContext.DatabaseContext.CanConnect && ExecuteWhenDatabaseNotConfigured) { return true; } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IScheduledTasksSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IScheduledTasksSection.cs index 9d01549a5c..8b25e11e91 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IScheduledTasksSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IScheduledTasksSection.cs @@ -5,5 +5,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings public interface IScheduledTasksSection : IUmbracoConfigurationSection { IEnumerable Tasks { get; } + + string BaseUrl { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ScheduledTasksElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ScheduledTasksElement.cs index 3571e2d7ed..0598aeec9e 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ScheduledTasksElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ScheduledTasksElement.cs @@ -16,5 +16,11 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { get { return Tasks; } } + + [ConfigurationProperty("baseUrl", IsRequired = false, DefaultValue = null)] + public string BaseUrl + { + get { return (string)base["baseUrl"]; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/DatabaseContext.cs b/src/Umbraco.Core/DatabaseContext.cs index 105d006656..fad96b849d 100644 --- a/src/Umbraco.Core/DatabaseContext.cs +++ b/src/Umbraco.Core/DatabaseContext.cs @@ -26,6 +26,9 @@ namespace Umbraco.Core { private readonly IDatabaseFactory _factory; private bool _configured; + private bool _canConnect; + private volatile bool _connectCheck = false; + private readonly object _locker = new object(); private string _connectionString; private string _providerName; private DatabaseSchemaResult _result; @@ -56,6 +59,32 @@ namespace Umbraco.Core get { return _configured; } } + /// + /// Determines if the db can be connected to + /// + public bool CanConnect + { + get + { + if (IsDatabaseConfigured == false) return false; + + //double check lock so that it is only checked once and is fast + if (_connectCheck == false) + { + lock (_locker) + { + if (_canConnect == false) + { + _canConnect = DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DatabaseProvider); + _connectCheck = true; + } + } + } + + return _canConnect; + } + } + /// /// Gets the configured umbraco db connection string. /// diff --git a/src/Umbraco.Core/Persistence/DbConnectionExtensions.cs b/src/Umbraco.Core/Persistence/DbConnectionExtensions.cs index f7bd9e31c1..c4ca875eb2 100644 --- a/src/Umbraco.Core/Persistence/DbConnectionExtensions.cs +++ b/src/Umbraco.Core/Persistence/DbConnectionExtensions.cs @@ -75,7 +75,7 @@ namespace Umbraco.Core.Persistence connection.Open(); connection.Close(); } - catch (SqlException) + catch (DbException) { return false; } diff --git a/src/Umbraco.Core/Services/ApplicationTreeService.cs b/src/Umbraco.Core/Services/ApplicationTreeService.cs index dbf5c9b85a..5278990d6a 100644 --- a/src/Umbraco.Core/Services/ApplicationTreeService.cs +++ b/src/Umbraco.Core/Services/ApplicationTreeService.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Cache; using File = System.IO.File; namespace Umbraco.Core.Services @@ -15,15 +16,18 @@ namespace Umbraco.Core.Services internal class ApplicationTreeService : IApplicationTreeService { private readonly CacheHelper _cache; + private IEnumerable _allAvailableTrees; + private volatile bool _isInitialized = false; + 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 @@ -45,74 +49,101 @@ namespace Umbraco.Core.Services } /// - /// The cache storage for all application trees + /// The main entry point to get application trees /// + /// + /// This lazily on first access will scan for plugin trees and ensure the trees.config is up-to-date with the plugins. If plugins + /// haven't changed on disk then the file will not be saved. The trees are all then loaded from this config file into cache and returned. + /// private List GetAppTrees() { - return _cache.GetCacheItem( + return _cache.RuntimeCache.GetCacheItem>( CacheKeys.ApplicationTreeCacheKey, () => + { + var list = ReadFromXmlAndSort(); + + //On first access we need to do some initialization + if (_isInitialized == false) { - var list = new List(); - - LoadXml(doc => + lock (Locker) + { + if (_isInitialized == false) { - foreach (var addElement in doc.Root.Elements("add").OrderBy(x => - { - var sortOrderAttr = x.Attribute("sortOrder"); - return sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0; - })) + //now we can check the non-volatile flag + if (_allAvailableTrees != null) { - var applicationAlias = (string) addElement.Attribute("application"); - var type = (string) addElement.Attribute("type"); - var assembly = (string) addElement.Attribute("assembly"); + var hasChanges = false; - var clrType = Type.GetType(type); - if (clrType == null) + LoadXml(doc => { - LogHelper.Warn("The tree definition: " + addElement.ToString() + " could not be resolved to a .Net object type"); - continue; - } + //Now, load in the xml structure and update it with anything that is not declared there and save the file. - //check if the tree definition (applicationAlias + type + assembly) is already in the list + //NOTE: On the first iteration here, it will lazily scan all trees, etc... this is because this ienumerable is lazy + // based on the ApplicationTreeRegistrar - 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. - if (list.Any(tree => tree.ApplicationAlias.InvariantEquals(applicationAlias) && tree.GetRuntimeType() == clrType) == false) + //Get all the trees not registered in the config + var unregistered = _allAvailableTrees + .Where(x => list.Any(l => l.Alias == x.Alias) == false) + .ToArray(); + + hasChanges = unregistered.Any(); + + if (hasChanges == false) return false; + + //add the unregistered ones to the list and re-save the file if any changes were found + var count = 0; + foreach (var tree in unregistered) + { + doc.Root.Add(new XElement("add", + new XAttribute("initialize", tree.Initialize), + new XAttribute("sortOrder", tree.SortOrder), + new XAttribute("alias", tree.Alias), + new XAttribute("application", tree.ApplicationAlias), + new XAttribute("title", tree.Title), + new XAttribute("iconClosed", tree.IconClosed), + new XAttribute("iconOpen", tree.IconOpened), + new XAttribute("type", tree.Type))); + count++; + } + + //don't save if there's no changes + return count > 0; + }, true); + + if (hasChanges) { - list.Add(new ApplicationTree( - addElement.Attribute("initialize") == null || Convert.ToBoolean(addElement.Attribute("initialize").Value), - addElement.Attribute("sortOrder") != null ? Convert.ToByte(addElement.Attribute("sortOrder").Value) : (byte) 0, - addElement.Attribute("application").Value, - addElement.Attribute("alias").Value, - addElement.Attribute("title").Value, - addElement.Attribute("iconClosed").Value, - addElement.Attribute("iconOpen").Value, - addElement.Attribute("type").Value)); + //If there were changes, we need to re-read the structures from the XML + list = ReadFromXmlAndSort(); } } - }, false); - return list; - }); + _isInitialized = true; + } + } + } + + + return list; + + + }); } - public void Intitialize(IEnumerable existingTrees) + /// + /// Initializes the service with any trees found in plugins + /// + /// + /// A collection of all available tree found in assemblies in the application + /// + /// + /// This will update the trees.config with the found tree plugins that are not currently listed in the file when the first + /// access is made to resolve the tree collection + /// + public void Intitialize(IEnumerable allAvailableTrees) { - LoadXml(doc => - { - foreach (var tree in existingTrees) - { - doc.Root.Add(new XElement("add", - new XAttribute("initialize", tree.Initialize), - new XAttribute("sortOrder", tree.SortOrder), - new XAttribute("alias", tree.Alias), - new XAttribute("application", tree.ApplicationAlias), - new XAttribute("title", tree.Title), - new XAttribute("iconClosed", tree.IconClosed), - new XAttribute("iconOpen", tree.IconOpened), - new XAttribute("type", tree.Type))); - } - - }, true); + _allAvailableTrees = allAvailableTrees; } /// @@ -134,16 +165,19 @@ namespace Umbraco.Core.Services if (el == null) { - doc.Root.Add(new XElement("add", - new XAttribute("initialize", initialize), - new XAttribute("sortOrder", sortOrder), - new XAttribute("alias", alias), - new XAttribute("application", applicationAlias), - new XAttribute("title", title), - new XAttribute("iconClosed", iconClosed), - new XAttribute("iconOpen", iconOpened), - new XAttribute("type", type))); + doc.Root.Add(new XElement("add", + new XAttribute("initialize", initialize), + new XAttribute("sortOrder", sortOrder), + new XAttribute("alias", alias), + new XAttribute("application", applicationAlias), + new XAttribute("title", title), + new XAttribute("iconClosed", iconClosed), + new XAttribute("iconOpen", iconOpened), + new XAttribute("type", type))); } + + return true; + }, true); OnNew(new ApplicationTree(initialize, sortOrder, applicationAlias, alias, title, iconClosed, iconOpened, type), new EventArgs()); @@ -161,7 +195,7 @@ namespace Umbraco.Core.Services if (el != null) { el.RemoveAttributes(); - + el.Add(new XAttribute("initialize", tree.Initialize)); el.Add(new XAttribute("sortOrder", tree.SortOrder)); el.Add(new XAttribute("alias", tree.Alias)); @@ -172,6 +206,8 @@ namespace Umbraco.Core.Services el.Add(new XAttribute("type", tree.Type)); } + return true; + }, true); OnUpdated(tree, new EventArgs()); @@ -181,11 +217,16 @@ namespace Umbraco.Core.Services /// Deletes this instance. /// public void DeleteTree(ApplicationTree tree) - { + { LoadXml(doc => { - doc.Root.Elements("add").Where(x => x.Attribute("application") != null && x.Attribute("application").Value == tree.ApplicationAlias && - x.Attribute("alias") != null && x.Attribute("alias").Value == tree.Alias).Remove(); + doc.Root.Elements("add") + .Where(x => x.Attribute("application") != null + && x.Attribute("application").Value == tree.ApplicationAlias + && x.Attribute("alias") != null && x.Attribute("alias").Value == tree.Alias).Remove(); + + return true; + }, true); OnDeleted(tree, new EventArgs()); @@ -231,31 +272,44 @@ namespace Umbraco.Core.Services { var list = GetAppTrees().FindAll( t => - { - if (onlyInitialized) - return (t.ApplicationAlias == applicationAlias && t.Initialize); - return (t.ApplicationAlias == applicationAlias); - } + { + if (onlyInitialized) + return (t.ApplicationAlias == applicationAlias && t.Initialize); + return (t.ApplicationAlias == applicationAlias); + } ); return list.OrderBy(x => x.SortOrder).ToArray(); } - internal void LoadXml(Action callback, bool saveAfterCallback) + /// + /// Loads in the xml structure from disk if one is found, otherwise loads in an empty xml structure, calls the + /// callback with the xml document and saves the structure back to disk if saveAfterCallback is true. + /// + /// + /// + internal void LoadXml(Func callback, bool saveAfterCallbackIfChanges) { lock (Locker) { var doc = File.Exists(TreeConfigFilePath) ? XDocument.Load(TreeConfigFilePath) : XDocument.Parse(""); + if (doc.Root != null) { - callback.Invoke(doc); + var hasChanges = callback.Invoke(doc); - if (saveAfterCallback) + if (saveAfterCallbackIfChanges && hasChanges + //Don't save it if it is empty, in some very rare cases if the app domain get's killed in the middle of this process + // in some insane way the file saved will be empty. I'm pretty sure it's not actually anything to do with the xml doc and + // more about the IO trying to save the XML doc, but it doesn't hurt to check. + && doc.Root != null && doc.Root.Elements().Any()) { + //ensures the folder exists Directory.CreateDirectory(Path.GetDirectoryName(TreeConfigFilePath)); + //saves it doc.Save(TreeConfigFilePath); //remove the cache now that it has changed SD: I'm leaving this here even though it @@ -266,6 +320,54 @@ namespace Umbraco.Core.Services } } + private List ReadFromXmlAndSort() + { + var list = new List(); + + //read in the xml file containing trees and convert them all to ApplicationTree instances + 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 applicationAlias = (string)addElement.Attribute("application"); + var type = (string)addElement.Attribute("type"); + var assembly = (string)addElement.Attribute("assembly"); + + var clrType = Type.GetType(type); + if (clrType == null) + { + LogHelper.Warn("The tree definition: " + addElement.ToString() + " could not be resolved to a .Net object type"); + continue; + } + + //check if the tree definition (applicationAlias + type + assembly) is already in the list + + if (list.Any(tree => tree.ApplicationAlias.InvariantEquals(applicationAlias) && tree.GetRuntimeType() == clrType) == false) + { + list.Add(new ApplicationTree( + addElement.Attribute("initialize") == null || Convert.ToBoolean(addElement.Attribute("initialize").Value), + addElement.Attribute("sortOrder") != null ? Convert.ToByte(addElement.Attribute("sortOrder").Value) : (byte)0, + addElement.Attribute("application").Value, + addElement.Attribute("alias").Value, + addElement.Attribute("title").Value, + addElement.Attribute("iconClosed").Value, + addElement.Attribute("iconOpen").Value, + addElement.Attribute("type").Value)); + } + } + + return false; + + }, false); + + return list; + } + + internal static event TypedEventHandler Deleted; private static void OnDeleted(ApplicationTree app, EventArgs args) { diff --git a/src/Umbraco.Core/Services/IApplicationTreeService.cs b/src/Umbraco.Core/Services/IApplicationTreeService.cs index c7aecec6fb..6a4d2bda33 100644 --- a/src/Umbraco.Core/Services/IApplicationTreeService.cs +++ b/src/Umbraco.Core/Services/IApplicationTreeService.cs @@ -5,7 +5,17 @@ namespace Umbraco.Core.Services { public interface IApplicationTreeService { - void Intitialize(IEnumerable existingTrees); + /// + /// Initializes the service with any trees found in plugins + /// + /// + /// A collection of all available tree found in assemblies in the application + /// + /// + /// This will update the trees.config with the found tree plugins that are not currently listed in the file when the first + /// access is made to resolve the tree collection + /// + void Intitialize(IEnumerable allAvailableTrees); /// /// Creates a new application tree. 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 afc44a3359..79280b371f 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -164,7 +164,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.Core/Sync/ServerEnvironmentHelper.cs b/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs index 63e5fbc553..52480a1ca7 100644 --- a/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs +++ b/src/Umbraco.Core/Sync/ServerEnvironmentHelper.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Web; using System.Xml; using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; namespace Umbraco.Core.Sync @@ -19,28 +20,24 @@ namespace Umbraco.Core.Sync ///
/// The full base url including schema (i.e. http://myserver:80/umbraco ) - or null if the url /// cannot be determined at the moment (usually because the first request has not properly completed yet). - public static string GetCurrentServerUmbracoBaseUrl() + public static string GetCurrentServerUmbracoBaseUrl(ApplicationContext appContext, IUmbracoSettingsSection settings) { - var status = GetStatus(); + var status = GetStatus(settings); if (status == CurrentServerEnvironmentStatus.Single) { - // single install, return null if no original url, else use original url as base + // single install, return null if no config/original url, else use config/original url as base // use http or https as appropriate - return string.IsNullOrWhiteSpace(ApplicationContext.Current.OriginalRequestUrl) - ? null // not initialized yet - : string.Format("http{0}://{1}", GlobalSettings.UseSSL ? "s" : "", ApplicationContext.Current.OriginalRequestUrl); + return GetBaseUrl(appContext, settings); } - var servers = UmbracoConfig.For.UmbracoSettings().DistributedCall.Servers.ToArray(); + var servers = settings.DistributedCall.Servers.ToArray(); if (servers.Any() == false) { - // cannot be determined, return null if no original url, else use original url as base + // cannot be determined, return null if no config/original url, else use config/original url as base // use http or https as appropriate - return string.IsNullOrWhiteSpace(ApplicationContext.Current.OriginalRequestUrl) - ? null // not initialized yet - : string.Format("http{0}://{1}", GlobalSettings.UseSSL ? "s" : "", ApplicationContext.Current.OriginalRequestUrl); + return GetBaseUrl(appContext, settings); } foreach (var server in servers) @@ -65,25 +62,23 @@ namespace Umbraco.Core.Sync } } - // cannot be determined, return null if no original url, else use original url as base + // cannot be determined, return null if no config/original url, else use config/original url as base // use http or https as appropriate - return string.IsNullOrWhiteSpace(ApplicationContext.Current.OriginalRequestUrl) - ? null // not initialized yet - : string.Format("http{0}://{1}", GlobalSettings.UseSSL ? "s" : "", ApplicationContext.Current.OriginalRequestUrl); + return GetBaseUrl(appContext, settings); } /// /// Returns the current environment status for the current server /// /// - public static CurrentServerEnvironmentStatus GetStatus() + public static CurrentServerEnvironmentStatus GetStatus(IUmbracoSettingsSection settings) { - if (UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled == false) + if (settings.DistributedCall.Enabled == false) { return CurrentServerEnvironmentStatus.Single; } - var servers = UmbracoConfig.For.UmbracoSettings().DistributedCall.Servers.ToArray(); + var servers = settings.DistributedCall.Servers.ToArray(); if (servers.Any() == false) { @@ -118,5 +113,21 @@ namespace Umbraco.Core.Sync return CurrentServerEnvironmentStatus.Slave; } + + private static string GetBaseUrl(ApplicationContext appContext, IUmbracoSettingsSection settings) + { + return ( + // is config empty? + settings.ScheduledTasks.BaseUrl.IsNullOrWhiteSpace() + // is the orig req empty? + ? appContext.OriginalRequestUrl.IsNullOrWhiteSpace() + // we've got nothing + ? null + //the orig req url is not null, use that + : string.Format("http{0}://{1}", GlobalSettings.UseSSL ? "s" : "", appContext.OriginalRequestUrl) + // the config has been specified, use that + : string.Format("http{0}://{1}", GlobalSettings.UseSSL ? "s" : "", settings.ScheduledTasks.BaseUrl)) + .EnsureEndsWith('/'); + } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/GlobalSettingsTests.cs b/src/Umbraco.Tests/Configurations/GlobalSettingsTests.cs similarity index 95% rename from src/Umbraco.Tests/GlobalSettingsTests.cs rename to src/Umbraco.Tests/Configurations/GlobalSettingsTests.cs index 7f97466bae..b320c979ef 100644 --- a/src/Umbraco.Tests/GlobalSettingsTests.cs +++ b/src/Umbraco.Tests/Configurations/GlobalSettingsTests.cs @@ -1,120 +1,119 @@ -using System.Configuration; -using System.Web.Routing; -using NUnit.Framework; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Tests.TestHelpers; -using System.Web.Mvc; - -namespace Umbraco.Tests -{ - [TestFixture] - public class GlobalSettingsTests : BaseWebTest - { - - public override void Initialize() - { - base.Initialize(); - SettingsForTests.UmbracoPath = "~/umbraco"; - } - - public override void TearDown() - { - //ensure this is reset - SystemDirectories.Root = null; - SettingsForTests.UmbracoPath = "~/umbraco"; - //reset the app config - base.TearDown(); - - } - - [Test] - public void Is_Debug_Mode() - { - Assert.That(Umbraco.Core.Configuration.GlobalSettings.DebugMode, Is.EqualTo(true)); - } - - [Ignore] - [Test] - public void Is_Version_From_Assembly_Correct() - { - Assert.That(UmbracoVersion.Current.ToString(3), Is.EqualTo("6.0.0")); - } - - [TestCase("~/umbraco", "/", "umbraco")] - [TestCase("~/umbraco", "/MyVirtualDir", "umbraco")] - [TestCase("~/customPath", "/MyVirtualDir/", "custompath")] - [TestCase("~/some-wacky/nestedPath", "/MyVirtualDir", "some-wacky-nestedpath")] - [TestCase("~/some-wacky/nestedPath", "/MyVirtualDir/NestedVDir/", "some-wacky-nestedpath")] - public void Umbraco_Mvc_Area(string path, string rootPath, string outcome) - { - SettingsForTests.UmbracoPath = path; - SystemDirectories.Root = rootPath; - Assert.AreEqual(outcome, Umbraco.Core.Configuration.GlobalSettings.UmbracoMvcArea); - } - - [TestCase("/umbraco/umbraco.aspx")] - [TestCase("/umbraco/editContent.aspx")] - [TestCase("/install/default.aspx")] - [TestCase("/install/")] - [TestCase("/install")] - [TestCase("/install/?installStep=asdf")] - [TestCase("/install/test.aspx")] - [TestCase("/config/splashes/booting.aspx")] - public void Is_Reserved_Path_Or_Url(string url) - { - Assert.IsTrue(Umbraco.Core.Configuration.GlobalSettings.IsReservedPathOrUrl(url)); - } - - [TestCase("/umbraco_client/Tree/treeIcons.css")] - [TestCase("/umbraco_client/Tree/Themes/umbraco/style.css")] - [TestCase("/umbraco_client/scrollingmenu/style.css")] - [TestCase("/base/somebasehandler")] - [TestCase("/")] - [TestCase("/home.aspx")] - [TestCase("/umbraco-test")] - [TestCase("/install-test")] - [TestCase("/install.aspx")] - public void Is_Not_Reserved_Path_Or_Url(string url) - { - Assert.IsFalse(Umbraco.Core.Configuration.GlobalSettings.IsReservedPathOrUrl(url)); - } - - - [TestCase("/Do/Not/match", false)] - [TestCase("/Umbraco/RenderMvcs", false)] - [TestCase("/Umbraco/RenderMvc", true)] - [TestCase("/Umbraco/RenderMvc/Index", true)] - [TestCase("/Umbraco/RenderMvc/Index/1234", true)] - [TestCase("/Umbraco/RenderMvc/Index/1234/9876", false)] - [TestCase("/api", true)] - [TestCase("/api/WebApiTest", true)] - [TestCase("/api/WebApiTest/1234", true)] - [TestCase("/api/WebApiTest/Index/1234", false)] - public void Is_Reserved_By_Route(string url, bool shouldMatch) - { - //reset the app config, we only want to test routes not the hard coded paths - Umbraco.Core.Configuration.GlobalSettings.ReservedPaths = ""; - Umbraco.Core.Configuration.GlobalSettings.ReservedUrls = ""; - - var routes = new RouteCollection(); - - routes.MapRoute( - "Umbraco_default", - "Umbraco/RenderMvc/{action}/{id}", - new { controller = "RenderMvc", action = "Index", id = UrlParameter.Optional }); - routes.MapRoute( - "WebAPI", - "api/{controller}/{id}", - new { controller = "WebApiTestController", action = "Index", id = UrlParameter.Optional }); - - - var context = new FakeHttpContextFactory(url); - - - Assert.AreEqual( - shouldMatch, - Umbraco.Core.Configuration.GlobalSettings.IsReservedPathOrUrl(url, context.HttpContext, routes)); - } - } +using System.Web.Mvc; +using System.Web.Routing; +using NUnit.Framework; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Tests.TestHelpers; + +namespace Umbraco.Tests.Configurations +{ + [TestFixture] + public class GlobalSettingsTests : BaseWebTest + { + + public override void Initialize() + { + base.Initialize(); + SettingsForTests.UmbracoPath = "~/umbraco"; + } + + public override void TearDown() + { + //ensure this is reset + SystemDirectories.Root = null; + SettingsForTests.UmbracoPath = "~/umbraco"; + //reset the app config + base.TearDown(); + + } + + [Test] + public void Is_Debug_Mode() + { + Assert.That(Umbraco.Core.Configuration.GlobalSettings.DebugMode, Is.EqualTo(true)); + } + + [Ignore] + [Test] + public void Is_Version_From_Assembly_Correct() + { + Assert.That(UmbracoVersion.Current.ToString(3), Is.EqualTo("6.0.0")); + } + + [TestCase("~/umbraco", "/", "umbraco")] + [TestCase("~/umbraco", "/MyVirtualDir", "umbraco")] + [TestCase("~/customPath", "/MyVirtualDir/", "custompath")] + [TestCase("~/some-wacky/nestedPath", "/MyVirtualDir", "some-wacky-nestedpath")] + [TestCase("~/some-wacky/nestedPath", "/MyVirtualDir/NestedVDir/", "some-wacky-nestedpath")] + public void Umbraco_Mvc_Area(string path, string rootPath, string outcome) + { + SettingsForTests.UmbracoPath = path; + SystemDirectories.Root = rootPath; + Assert.AreEqual(outcome, Umbraco.Core.Configuration.GlobalSettings.UmbracoMvcArea); + } + + [TestCase("/umbraco/umbraco.aspx")] + [TestCase("/umbraco/editContent.aspx")] + [TestCase("/install/default.aspx")] + [TestCase("/install/")] + [TestCase("/install")] + [TestCase("/install/?installStep=asdf")] + [TestCase("/install/test.aspx")] + [TestCase("/config/splashes/booting.aspx")] + public void Is_Reserved_Path_Or_Url(string url) + { + Assert.IsTrue(Umbraco.Core.Configuration.GlobalSettings.IsReservedPathOrUrl(url)); + } + + [TestCase("/umbraco_client/Tree/treeIcons.css")] + [TestCase("/umbraco_client/Tree/Themes/umbraco/style.css")] + [TestCase("/umbraco_client/scrollingmenu/style.css")] + [TestCase("/base/somebasehandler")] + [TestCase("/")] + [TestCase("/home.aspx")] + [TestCase("/umbraco-test")] + [TestCase("/install-test")] + [TestCase("/install.aspx")] + public void Is_Not_Reserved_Path_Or_Url(string url) + { + Assert.IsFalse(Umbraco.Core.Configuration.GlobalSettings.IsReservedPathOrUrl(url)); + } + + + [TestCase("/Do/Not/match", false)] + [TestCase("/Umbraco/RenderMvcs", false)] + [TestCase("/Umbraco/RenderMvc", true)] + [TestCase("/Umbraco/RenderMvc/Index", true)] + [TestCase("/Umbraco/RenderMvc/Index/1234", true)] + [TestCase("/Umbraco/RenderMvc/Index/1234/9876", false)] + [TestCase("/api", true)] + [TestCase("/api/WebApiTest", true)] + [TestCase("/api/WebApiTest/1234", true)] + [TestCase("/api/WebApiTest/Index/1234", false)] + public void Is_Reserved_By_Route(string url, bool shouldMatch) + { + //reset the app config, we only want to test routes not the hard coded paths + Umbraco.Core.Configuration.GlobalSettings.ReservedPaths = ""; + Umbraco.Core.Configuration.GlobalSettings.ReservedUrls = ""; + + var routes = new RouteCollection(); + + routes.MapRoute( + "Umbraco_default", + "Umbraco/RenderMvc/{action}/{id}", + new { controller = "RenderMvc", action = "Index", id = UrlParameter.Optional }); + routes.MapRoute( + "WebAPI", + "api/{controller}/{id}", + new { controller = "WebApiTestController", action = "Index", id = UrlParameter.Optional }); + + + var context = new FakeHttpContextFactory(url); + + + Assert.AreEqual( + shouldMatch, + Umbraco.Core.Configuration.GlobalSettings.IsReservedPathOrUrl(url, context.HttpContext, routes)); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Surface/PluginControllerAreaTests.cs b/src/Umbraco.Tests/Controllers/PluginControllerAreaTests.cs similarity index 92% rename from src/Umbraco.Tests/Surface/PluginControllerAreaTests.cs rename to src/Umbraco.Tests/Controllers/PluginControllerAreaTests.cs index 2ed7bf9876..a2ad7220a5 100644 --- a/src/Umbraco.Tests/Surface/PluginControllerAreaTests.cs +++ b/src/Umbraco.Tests/Controllers/PluginControllerAreaTests.cs @@ -1,90 +1,87 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using NUnit.Framework; -using Umbraco.Tests.TestHelpers; -using Umbraco.Web; -using Umbraco.Web.Mvc; - -namespace Umbraco.Tests.Surface -{ - [TestFixture] - public class PluginControllerAreaTests : BaseWebTest - { - - [Test] - public void Ensure_Same_Area1() - { - Assert.Throws(() => - new PluginControllerArea(new PluginControllerMetadata[] - { - PluginController.GetMetadata(typeof(Plugin1Controller)), - PluginController.GetMetadata(typeof(Plugin2Controller)), - PluginController.GetMetadata(typeof(Plugin3Controller)) //not same area - })); - } - - [Test] - public void Ensure_Same_Area3() - { - Assert.Throws(() => - new PluginControllerArea(new PluginControllerMetadata[] - { - PluginController.GetMetadata(typeof(Plugin1Controller)), - PluginController.GetMetadata(typeof(Plugin2Controller)), - PluginController.GetMetadata(typeof(Plugin4Controller)) //no area assigned - })); - } - - [Test] - public void Ensure_Same_Area2() - { - var area = new PluginControllerArea(new PluginControllerMetadata[] - { - PluginController.GetMetadata(typeof(Plugin1Controller)), - PluginController.GetMetadata(typeof(Plugin2Controller)) - }); - Assert.Pass(); - } - - #region Test classes - - [PluginController("Area1")] - public class Plugin1Controller : PluginController - { - public Plugin1Controller(UmbracoContext umbracoContext) : base(umbracoContext) - { - } - } - - [PluginController("Area1")] - public class Plugin2Controller : PluginController - { - public Plugin2Controller(UmbracoContext umbracoContext) - : base(umbracoContext) - { - } - } - - [PluginController("Area2")] - public class Plugin3Controller : PluginController - { - public Plugin3Controller(UmbracoContext umbracoContext) - : base(umbracoContext) - { - } - } - - public class Plugin4Controller : PluginController - { - public Plugin4Controller(UmbracoContext umbracoContext) - : base(umbracoContext) - { - } - } - - #endregion - - } -} +using System; +using NUnit.Framework; +using Umbraco.Tests.TestHelpers; +using Umbraco.Web; +using Umbraco.Web.Mvc; + +namespace Umbraco.Tests.Controllers +{ + [TestFixture] + public class PluginControllerAreaTests : BaseWebTest + { + + [Test] + public void Ensure_Same_Area1() + { + Assert.Throws(() => + new PluginControllerArea(new PluginControllerMetadata[] + { + PluginController.GetMetadata(typeof(Plugin1Controller)), + PluginController.GetMetadata(typeof(Plugin2Controller)), + PluginController.GetMetadata(typeof(Plugin3Controller)) //not same area + })); + } + + [Test] + public void Ensure_Same_Area3() + { + Assert.Throws(() => + new PluginControllerArea(new PluginControllerMetadata[] + { + PluginController.GetMetadata(typeof(Plugin1Controller)), + PluginController.GetMetadata(typeof(Plugin2Controller)), + PluginController.GetMetadata(typeof(Plugin4Controller)) //no area assigned + })); + } + + [Test] + public void Ensure_Same_Area2() + { + var area = new PluginControllerArea(new PluginControllerMetadata[] + { + PluginController.GetMetadata(typeof(Plugin1Controller)), + PluginController.GetMetadata(typeof(Plugin2Controller)) + }); + Assert.Pass(); + } + + #region Test classes + + [PluginController("Area1")] + public class Plugin1Controller : PluginController + { + public Plugin1Controller(UmbracoContext umbracoContext) : base(umbracoContext) + { + } + } + + [PluginController("Area1")] + public class Plugin2Controller : PluginController + { + public Plugin2Controller(UmbracoContext umbracoContext) + : base(umbracoContext) + { + } + } + + [PluginController("Area2")] + public class Plugin3Controller : PluginController + { + public Plugin3Controller(UmbracoContext umbracoContext) + : base(umbracoContext) + { + } + } + + public class Plugin4Controller : PluginController + { + public Plugin4Controller(UmbracoContext umbracoContext) + : base(umbracoContext) + { + } + } + + #endregion + + } +} diff --git a/src/Umbraco.Tests/Sync/DistributedCacheTests.cs b/src/Umbraco.Tests/DistributedCache/DistributedCacheTests.cs similarity index 89% rename from src/Umbraco.Tests/Sync/DistributedCacheTests.cs rename to src/Umbraco.Tests/DistributedCache/DistributedCacheTests.cs index a897f59dae..c714cd0a2e 100644 --- a/src/Umbraco.Tests/Sync/DistributedCacheTests.cs +++ b/src/Umbraco.Tests/DistributedCache/DistributedCacheTests.cs @@ -1,211 +1,210 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.ObjectResolution; -using Umbraco.Core.Sync; -using Umbraco.Web.Cache; -using umbraco.interfaces; - -namespace Umbraco.Tests.Sync -{ - /// - /// Ensures that calls to DistributedCache methods carry through to the IServerMessenger correctly - /// - [TestFixture] - public class DistributedCacheTests - { - [SetUp] - public void Setup() - { - ServerRegistrarResolver.Current = new ServerRegistrarResolver( - new TestServerRegistrar()); - ServerMessengerResolver.Current = new ServerMessengerResolver( - new TestServerMessenger()); - CacheRefreshersResolver.Current = new CacheRefreshersResolver(() => new[] { typeof(TestCacheRefresher) }); - Resolution.Freeze(); - } - - [TearDown] - public void Teardown() - { - ServerRegistrarResolver.Reset(); - ServerMessengerResolver.Reset(); - CacheRefreshersResolver.Reset(); - } - - [Test] - public void RefreshIntId() - { - for (var i = 1; i < 11; i++) - { - DistributedCache.Instance.Refresh(Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73"), i); - } - Assert.AreEqual(10, ((TestServerMessenger)ServerMessengerResolver.Current.Messenger).IntIdsRefreshed.Count); - } - - [Test] - public void RefreshIntIdFromObject() - { - for (var i = 0; i < 10; i++) - { - DistributedCache.Instance.Refresh( - Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73"), - x => x.Id, - new TestObjectWithId{Id = i}); - } - Assert.AreEqual(10, ((TestServerMessenger)ServerMessengerResolver.Current.Messenger).IntIdsRefreshed.Count); - } - - [Test] - public void RefreshGuidId() - { - for (var i = 0; i < 11; i++) - { - DistributedCache.Instance.Refresh(Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73"), Guid.NewGuid()); - } - Assert.AreEqual(11, ((TestServerMessenger)ServerMessengerResolver.Current.Messenger).GuidIdsRefreshed.Count); - } - - [Test] - public void RemoveIds() - { - for (var i = 1; i < 13; i++) - { - DistributedCache.Instance.Remove(Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73"), i); - } - Assert.AreEqual(12, ((TestServerMessenger)ServerMessengerResolver.Current.Messenger).IntIdsRemoved.Count); - } - - [Test] - public void FullRefreshes() - { - for (var i = 0; i < 13; i++) - { - DistributedCache.Instance.RefreshAll(Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73")); - } - Assert.AreEqual(13, ((TestServerMessenger)ServerMessengerResolver.Current.Messenger).CountOfFullRefreshes); - } - - #region internal test classes - - internal class TestObjectWithId - { - public int Id { get; set; } - } - - internal class TestCacheRefresher : ICacheRefresher - { - public Guid UniqueIdentifier - { - get { return Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73"); } - } - public string Name - { - get { return "Test"; } - } - public void RefreshAll() - { - - } - - public void Refresh(int id) - { - - } - - public void Remove(int id) - { - - } - - public void Refresh(Guid id) - { - - } - } - - internal class TestServerMessenger : IServerMessenger - { - //used for tests - public List IntIdsRefreshed = new List(); - public List GuidIdsRefreshed = new List(); - public List IntIdsRemoved = new List(); - public List PayloadsRemoved = new List(); - public List PayloadsRefreshed = new List(); - public int CountOfFullRefreshes = 0; - - - public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, string jsonPayload) - { - PayloadsRefreshed.Add(jsonPayload); - } - - public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances) - { - IntIdsRefreshed.AddRange(instances.Select(getNumericId)); - } - - public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getGuidId, params T[] instances) - { - GuidIdsRefreshed.AddRange(instances.Select(getGuidId)); - } - - public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, string jsonPayload) - { - PayloadsRemoved.Add(jsonPayload); - } - - public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances) - { - IntIdsRemoved.AddRange(instances.Select(getNumericId)); - } - - public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) - { - IntIdsRemoved.AddRange(numericIds); - } - - public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) - { - IntIdsRefreshed.AddRange(numericIds); - } - - public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params Guid[] guidIds) - { - GuidIdsRefreshed.AddRange(guidIds); - } - - public void PerformRefreshAll(IEnumerable servers, ICacheRefresher refresher) - { - CountOfFullRefreshes++; - } - } - - internal class TestServerRegistrar : IServerRegistrar - { - public IEnumerable Registrations - { - get - { - return new List() - { - new TestServerAddress("localhost") - }; - } - } - } - - public class TestServerAddress : IServerAddress - { - public TestServerAddress(string address) - { - ServerAddress = address; - } - public string ServerAddress { get; private set; } - } - - #endregion - } +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.ObjectResolution; +using Umbraco.Core.Sync; +using umbraco.interfaces; + +namespace Umbraco.Tests.DistributedCache +{ + /// + /// Ensures that calls to DistributedCache methods carry through to the IServerMessenger correctly + /// + [TestFixture] + public class DistributedCacheTests + { + [SetUp] + public void Setup() + { + ServerRegistrarResolver.Current = new ServerRegistrarResolver( + new TestServerRegistrar()); + ServerMessengerResolver.Current = new ServerMessengerResolver( + new TestServerMessenger()); + CacheRefreshersResolver.Current = new CacheRefreshersResolver(() => new[] { typeof(TestCacheRefresher) }); + Resolution.Freeze(); + } + + [TearDown] + public void Teardown() + { + ServerRegistrarResolver.Reset(); + ServerMessengerResolver.Reset(); + CacheRefreshersResolver.Reset(); + } + + [Test] + public void RefreshIntId() + { + for (var i = 1; i < 11; i++) + { + Web.Cache.DistributedCache.Instance.Refresh(Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73"), i); + } + Assert.AreEqual(10, ((TestServerMessenger)ServerMessengerResolver.Current.Messenger).IntIdsRefreshed.Count); + } + + [Test] + public void RefreshIntIdFromObject() + { + for (var i = 0; i < 10; i++) + { + Web.Cache.DistributedCache.Instance.Refresh( + Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73"), + x => x.Id, + new TestObjectWithId{Id = i}); + } + Assert.AreEqual(10, ((TestServerMessenger)ServerMessengerResolver.Current.Messenger).IntIdsRefreshed.Count); + } + + [Test] + public void RefreshGuidId() + { + for (var i = 0; i < 11; i++) + { + Web.Cache.DistributedCache.Instance.Refresh(Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73"), Guid.NewGuid()); + } + Assert.AreEqual(11, ((TestServerMessenger)ServerMessengerResolver.Current.Messenger).GuidIdsRefreshed.Count); + } + + [Test] + public void RemoveIds() + { + for (var i = 1; i < 13; i++) + { + Web.Cache.DistributedCache.Instance.Remove(Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73"), i); + } + Assert.AreEqual(12, ((TestServerMessenger)ServerMessengerResolver.Current.Messenger).IntIdsRemoved.Count); + } + + [Test] + public void FullRefreshes() + { + for (var i = 0; i < 13; i++) + { + Web.Cache.DistributedCache.Instance.RefreshAll(Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73")); + } + Assert.AreEqual(13, ((TestServerMessenger)ServerMessengerResolver.Current.Messenger).CountOfFullRefreshes); + } + + #region internal test classes + + internal class TestObjectWithId + { + public int Id { get; set; } + } + + internal class TestCacheRefresher : ICacheRefresher + { + public Guid UniqueIdentifier + { + get { return Guid.Parse("E0F452CB-DCB2-4E84-B5A5-4F01744C5C73"); } + } + public string Name + { + get { return "Test"; } + } + public void RefreshAll() + { + + } + + public void Refresh(int id) + { + + } + + public void Remove(int id) + { + + } + + public void Refresh(Guid id) + { + + } + } + + internal class TestServerMessenger : IServerMessenger + { + //used for tests + public List IntIdsRefreshed = new List(); + public List GuidIdsRefreshed = new List(); + public List IntIdsRemoved = new List(); + public List PayloadsRemoved = new List(); + public List PayloadsRefreshed = new List(); + public int CountOfFullRefreshes = 0; + + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, string jsonPayload) + { + PayloadsRefreshed.Add(jsonPayload); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances) + { + IntIdsRefreshed.AddRange(instances.Select(getNumericId)); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getGuidId, params T[] instances) + { + GuidIdsRefreshed.AddRange(instances.Select(getGuidId)); + } + + public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, string jsonPayload) + { + PayloadsRemoved.Add(jsonPayload); + } + + public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances) + { + IntIdsRemoved.AddRange(instances.Select(getNumericId)); + } + + public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) + { + IntIdsRemoved.AddRange(numericIds); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) + { + IntIdsRefreshed.AddRange(numericIds); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params Guid[] guidIds) + { + GuidIdsRefreshed.AddRange(guidIds); + } + + public void PerformRefreshAll(IEnumerable servers, ICacheRefresher refresher) + { + CountOfFullRefreshes++; + } + } + + internal class TestServerRegistrar : IServerRegistrar + { + public IEnumerable Registrations + { + get + { + return new List() + { + new TestServerAddress("localhost") + }; + } + } + } + + public class TestServerAddress : IServerAddress + { + public TestServerAddress(string address) + { + ServerAddress = address; + } + public string ServerAddress { get; private set; } + } + + #endregion + } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Dynamics/QueryableExtensionTests.cs b/src/Umbraco.Tests/DynamicsAndReflection/QueryableExtensionTests.cs similarity index 95% rename from src/Umbraco.Tests/Dynamics/QueryableExtensionTests.cs rename to src/Umbraco.Tests/DynamicsAndReflection/QueryableExtensionTests.cs index d5343ff521..c2544421a0 100644 --- a/src/Umbraco.Tests/Dynamics/QueryableExtensionTests.cs +++ b/src/Umbraco.Tests/DynamicsAndReflection/QueryableExtensionTests.cs @@ -1,162 +1,158 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using NUnit.Framework; -using Umbraco.Core.Dynamics; -using Umbraco.Web.Dynamics; -using Umbraco.Core.Models; - -namespace Umbraco.Tests.Dynamics -{ - //NOTE: there's libraries in both Umbraco.Core.Dynamics and Umbraco.Web.Dynamics - the reason for this is that the Web.Dynamics - // started with the razor macro implementation and is modified with hard coded references to dynamic node and dynamic null, though it seems - // to still work for other regular classes I don't want to move it to the core without removing these references but that would require a lot of work. - - [TestFixture] - public class QueryableExtensionTests - { - - [Test] - public void Order_By_Test_Int() - { - var items = new List - { - new TestModel {Age = 10, Name = "test1", Female = false}, - new TestModel {Age = 31, Name = "someguy", Female = true}, - new TestModel {Age = 11, Name = "test2", Female = true}, - new TestModel {Age = 20, Name = "anothertest", Female = false}, - new TestModel {Age = 55, Name = "blah", Female = false}, - new TestModel {Age = 12, Name = "test3", Female = false} - }; - - var result = items.AsQueryable().OrderBy("Age").ToArray(); - - Assert.AreEqual(10, result.ElementAt(0).Age); - Assert.AreEqual(11, result.ElementAt(1).Age); - Assert.AreEqual(12, result.ElementAt(2).Age); - Assert.AreEqual(20, result.ElementAt(3).Age); - Assert.AreEqual(31, result.ElementAt(4).Age); - Assert.AreEqual(55, result.ElementAt(5).Age); - - } - - [Test] - public void Order_By_Test_String() - { - var items = new List - { - new TestModel {Age = 10, Name = "test1", Female = false}, - new TestModel {Age = 31, Name = "someguy", Female = true}, - new TestModel {Age = 11, Name = "test2", Female = true}, - new TestModel {Age = 20, Name = "anothertest", Female = false}, - new TestModel {Age = 55, Name = "blah", Female = false}, - new TestModel {Age = 12, Name = "test3", Female = false} - }; - - var result = items.AsQueryable().OrderBy("Name").ToArray(); - - Assert.AreEqual("anothertest", result.ElementAt(0).Name); - Assert.AreEqual("blah", result.ElementAt(1).Name); - Assert.AreEqual("someguy", result.ElementAt(2).Name); - Assert.AreEqual("test1", result.ElementAt(3).Name); - Assert.AreEqual("test2", result.ElementAt(4).Name); - Assert.AreEqual("test3", result.ElementAt(5).Name); - - } - - [Test] - public void Where_Test_String() - { - var items = new List - { - new TestModel {Age = 10, Name = "test1", Female = false}, - new TestModel {Age = 31, Name = "someguy", Female = true}, - new TestModel {Age = 11, Name = "test2", Female = true}, - new TestModel {Age = 20, Name = "anothertest", Female = false}, - new TestModel {Age = 55, Name = "blah", Female = false}, - new TestModel {Age = 12, Name = "test3", Female = false} - }; - - var result = items.AsQueryable().Where("Name = \"test1\"").ToArray(); - - Assert.AreEqual(1, result.Count()); - Assert.AreEqual("test1", result.ElementAt(0).Name); - - - } - - [Test] - public void Where_Test_String_With_Params() - { - var items = new List - { - new TestModel {Age = 10, Name = "test1", Female = false}, - new TestModel {Age = 31, Name = "someguy", Female = true}, - new TestModel {Age = 11, Name = "test2", Female = true}, - new TestModel {Age = 20, Name = "anothertest", Female = false}, - new TestModel {Age = 55, Name = "blah", Female = false}, - new TestModel {Age = 12, Name = "test3", Female = false} - }; - - //NOTE: Currently the object query structure is not supported - //var result = items.AsQueryable().Where("Name = @name", new {name = "test1"}).ToArray(); - var result = items.AsQueryable().Where("Name = @Name", new Dictionary { { "Name", "test1" } }).ToArray(); - - Assert.AreEqual(1, result.Count()); - Assert.AreEqual("test1", result.ElementAt(0).Name); - - - } - - [Test] - public void Where_Test_Int_With_Params() - { - var items = new List - { - new TestModel {Age = 10, Name = "test1", Female = false}, - new TestModel {Age = 31, Name = "someguy", Female = true}, - new TestModel {Age = 11, Name = "test2", Female = true}, - new TestModel {Age = 20, Name = "anothertest", Female = false}, - new TestModel {Age = 55, Name = "blah", Female = false}, - new TestModel {Age = 12, Name = "test3", Female = false} - }; - - var result = items.AsQueryable().Where("Age = @Age", new Dictionary { { "Age", 10 } }).ToArray(); - - Assert.AreEqual(1, result.Count()); - Assert.AreEqual("test1", result.ElementAt(0).Name); - - - } - - [Test] - public void Where_Test_Bool_With_Params() - { - var items = new List - { - new TestModel {Age = 10, Name = "test1", Female = false}, - new TestModel {Age = 31, Name = "someguy", Female = true}, - new TestModel {Age = 11, Name = "test2", Female = true}, - new TestModel {Age = 20, Name = "anothertest", Female = false}, - new TestModel {Age = 55, Name = "blah", Female = false}, - new TestModel {Age = 12, Name = "test3", Female = false} - }; - - var result = items.AsQueryable().Where("Female = @Female", new Dictionary { { "Female", true } }).ToArray(); - - Assert.AreEqual(2, result.Count()); - - - } - - private class TestModel - { - public string Name { get; set; } - public int Age { get; set; } - public bool Female { get; set; } - } - - } -} +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Umbraco.Core.Dynamics; +using Umbraco.Web.Dynamics; + +namespace Umbraco.Tests.DynamicsAndReflection +{ + //NOTE: there's libraries in both Umbraco.Core.Dynamics and Umbraco.Web.Dynamics - the reason for this is that the Web.Dynamics + // started with the razor macro implementation and is modified with hard coded references to dynamic node and dynamic null, though it seems + // to still work for other regular classes I don't want to move it to the core without removing these references but that would require a lot of work. + + [TestFixture] + public class QueryableExtensionTests + { + + [Test] + public void Order_By_Test_Int() + { + var items = new List + { + new TestModel {Age = 10, Name = "test1", Female = false}, + new TestModel {Age = 31, Name = "someguy", Female = true}, + new TestModel {Age = 11, Name = "test2", Female = true}, + new TestModel {Age = 20, Name = "anothertest", Female = false}, + new TestModel {Age = 55, Name = "blah", Female = false}, + new TestModel {Age = 12, Name = "test3", Female = false} + }; + + var result = items.AsQueryable().OrderBy("Age").ToArray(); + + Assert.AreEqual(10, result.ElementAt(0).Age); + Assert.AreEqual(11, result.ElementAt(1).Age); + Assert.AreEqual(12, result.ElementAt(2).Age); + Assert.AreEqual(20, result.ElementAt(3).Age); + Assert.AreEqual(31, result.ElementAt(4).Age); + Assert.AreEqual(55, result.ElementAt(5).Age); + + } + + [Test] + public void Order_By_Test_String() + { + var items = new List + { + new TestModel {Age = 10, Name = "test1", Female = false}, + new TestModel {Age = 31, Name = "someguy", Female = true}, + new TestModel {Age = 11, Name = "test2", Female = true}, + new TestModel {Age = 20, Name = "anothertest", Female = false}, + new TestModel {Age = 55, Name = "blah", Female = false}, + new TestModel {Age = 12, Name = "test3", Female = false} + }; + + var result = items.AsQueryable().OrderBy("Name").ToArray(); + + Assert.AreEqual("anothertest", result.ElementAt(0).Name); + Assert.AreEqual("blah", result.ElementAt(1).Name); + Assert.AreEqual("someguy", result.ElementAt(2).Name); + Assert.AreEqual("test1", result.ElementAt(3).Name); + Assert.AreEqual("test2", result.ElementAt(4).Name); + Assert.AreEqual("test3", result.ElementAt(5).Name); + + } + + [Test] + public void Where_Test_String() + { + var items = new List + { + new TestModel {Age = 10, Name = "test1", Female = false}, + new TestModel {Age = 31, Name = "someguy", Female = true}, + new TestModel {Age = 11, Name = "test2", Female = true}, + new TestModel {Age = 20, Name = "anothertest", Female = false}, + new TestModel {Age = 55, Name = "blah", Female = false}, + new TestModel {Age = 12, Name = "test3", Female = false} + }; + + var result = items.AsQueryable().Where("Name = \"test1\"").ToArray(); + + Assert.AreEqual(1, result.Count()); + Assert.AreEqual("test1", result.ElementAt(0).Name); + + + } + + [Test] + public void Where_Test_String_With_Params() + { + var items = new List + { + new TestModel {Age = 10, Name = "test1", Female = false}, + new TestModel {Age = 31, Name = "someguy", Female = true}, + new TestModel {Age = 11, Name = "test2", Female = true}, + new TestModel {Age = 20, Name = "anothertest", Female = false}, + new TestModel {Age = 55, Name = "blah", Female = false}, + new TestModel {Age = 12, Name = "test3", Female = false} + }; + + //NOTE: Currently the object query structure is not supported + //var result = items.AsQueryable().Where("Name = @name", new {name = "test1"}).ToArray(); + var result = items.AsQueryable().Where("Name = @Name", new Dictionary { { "Name", "test1" } }).ToArray(); + + Assert.AreEqual(1, result.Count()); + Assert.AreEqual("test1", result.ElementAt(0).Name); + + + } + + [Test] + public void Where_Test_Int_With_Params() + { + var items = new List + { + new TestModel {Age = 10, Name = "test1", Female = false}, + new TestModel {Age = 31, Name = "someguy", Female = true}, + new TestModel {Age = 11, Name = "test2", Female = true}, + new TestModel {Age = 20, Name = "anothertest", Female = false}, + new TestModel {Age = 55, Name = "blah", Female = false}, + new TestModel {Age = 12, Name = "test3", Female = false} + }; + + var result = items.AsQueryable().Where("Age = @Age", new Dictionary { { "Age", 10 } }).ToArray(); + + Assert.AreEqual(1, result.Count()); + Assert.AreEqual("test1", result.ElementAt(0).Name); + + + } + + [Test] + public void Where_Test_Bool_With_Params() + { + var items = new List + { + new TestModel {Age = 10, Name = "test1", Female = false}, + new TestModel {Age = 31, Name = "someguy", Female = true}, + new TestModel {Age = 11, Name = "test2", Female = true}, + new TestModel {Age = 20, Name = "anothertest", Female = false}, + new TestModel {Age = 55, Name = "blah", Female = false}, + new TestModel {Age = 12, Name = "test3", Female = false} + }; + + var result = items.AsQueryable().Where("Female = @Female", new Dictionary { { "Female", true } }).ToArray(); + + Assert.AreEqual(2, result.Count()); + + + } + + private class TestModel + { + public string Name { get; set; } + public int Age { get; set; } + public bool Female { get; set; } + } + + } +} diff --git a/src/Umbraco.Tests/MacroEngineFactoryTests.cs b/src/Umbraco.Tests/Macros/MacroEngineFactoryTests.cs similarity index 95% rename from src/Umbraco.Tests/MacroEngineFactoryTests.cs rename to src/Umbraco.Tests/Macros/MacroEngineFactoryTests.cs index e8d35e8255..2ab4c9f04e 100644 --- a/src/Umbraco.Tests/MacroEngineFactoryTests.cs +++ b/src/Umbraco.Tests/Macros/MacroEngineFactoryTests.cs @@ -1,139 +1,139 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Tests.TestHelpers; -using umbraco.cms.businesslogic.macro; -using umbraco.interfaces; - -namespace Umbraco.Tests -{ - [TestFixture] - public class MacroEngineFactoryTests - { - [SetUp] - public void Initialize() - { - TestHelper.SetupLog4NetForTests(); - - //this ensures its reset - PluginManager.Current = new PluginManager(false); - - //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver - PluginManager.Current.AssembliesToScan = new[] - { - this.GetType().Assembly - }; - } - - [TearDown] - public void TearDown() - { - PluginManager.Current = null; - } - - [Test] - public void Get_All() - { - var engines = MacroEngineFactory.GetAll(); - Assert.AreEqual(2, engines.Count()); - } - - [Test] - public void Get_Engine() - { - var engine1 = MacroEngineFactory.GetEngine("MacroEngine1"); - Assert.IsNotNull(engine1); - } - - [Test] - public void Get_By_Filename() - { - var engine1 = MacroEngineFactory.GetByFilename("test.me1"); - var engine2 = MacroEngineFactory.GetByFilename("test.me2"); - Assert.IsNotNull(engine1); - Assert.IsNotNull(engine2); - Assert.Throws(() => MacroEngineFactory.GetByFilename("test.blah")); - - } - - [Test] - public void Get_By_Extension() - { - var engine1 = MacroEngineFactory.GetByExtension("me1"); - var engine2 = MacroEngineFactory.GetByExtension("me2"); - Assert.IsNotNull(engine1); - Assert.IsNotNull(engine2); - Assert.Throws(() => MacroEngineFactory.GetByExtension("blah")); - } - - #region Classes for tests - public class MacroEngine1 : IMacroEngine - { - public string Name - { - get { return "MacroEngine1"; } - } - - public IEnumerable SupportedExtensions - { - get { return new[] {"me1"}; } - } - - public IEnumerable SupportedUIExtensions - { - get { throw new NotImplementedException(); } - } - - public Dictionary SupportedProperties - { - get { throw new NotImplementedException(); } - } - - public bool Validate(string code, string tempFileName, INode currentPage, out string errorMessage) - { - throw new NotImplementedException(); - } - - public string Execute(MacroModel macro, INode currentPage) - { - throw new NotImplementedException(); - } - } - - public class MacroEngine2 : IMacroEngine - { - public string Name - { - get { return "MacroEngine2"; } - } - - public IEnumerable SupportedExtensions - { - get { return new[] { "me2" }; } - } - - public IEnumerable SupportedUIExtensions - { - get { throw new NotImplementedException(); } - } - - public Dictionary SupportedProperties - { - get { throw new NotImplementedException(); } - } - - public bool Validate(string code, string tempFileName, INode currentPage, out string errorMessage) - { - throw new NotImplementedException(); - } - - public string Execute(MacroModel macro, INode currentPage) - { - throw new NotImplementedException(); - } - } - #endregion - } +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using umbraco.cms.businesslogic.macro; +using Umbraco.Core; +using umbraco.interfaces; +using Umbraco.Tests.TestHelpers; + +namespace Umbraco.Tests.Macros +{ + [TestFixture] + public class MacroEngineFactoryTests + { + [SetUp] + public void Initialize() + { + TestHelper.SetupLog4NetForTests(); + + //this ensures its reset + PluginManager.Current = new PluginManager(false); + + //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver + PluginManager.Current.AssembliesToScan = new[] + { + this.GetType().Assembly + }; + } + + [TearDown] + public void TearDown() + { + PluginManager.Current = null; + } + + [Test] + public void Get_All() + { + var engines = MacroEngineFactory.GetAll(); + Assert.AreEqual(2, engines.Count()); + } + + [Test] + public void Get_Engine() + { + var engine1 = MacroEngineFactory.GetEngine("MacroEngine1"); + Assert.IsNotNull(engine1); + } + + [Test] + public void Get_By_Filename() + { + var engine1 = MacroEngineFactory.GetByFilename("test.me1"); + var engine2 = MacroEngineFactory.GetByFilename("test.me2"); + Assert.IsNotNull(engine1); + Assert.IsNotNull(engine2); + Assert.Throws(() => MacroEngineFactory.GetByFilename("test.blah")); + + } + + [Test] + public void Get_By_Extension() + { + var engine1 = MacroEngineFactory.GetByExtension("me1"); + var engine2 = MacroEngineFactory.GetByExtension("me2"); + Assert.IsNotNull(engine1); + Assert.IsNotNull(engine2); + Assert.Throws(() => MacroEngineFactory.GetByExtension("blah")); + } + + #region Classes for tests + public class MacroEngine1 : IMacroEngine + { + public string Name + { + get { return "MacroEngine1"; } + } + + public IEnumerable SupportedExtensions + { + get { return new[] {"me1"}; } + } + + public IEnumerable SupportedUIExtensions + { + get { throw new NotImplementedException(); } + } + + public Dictionary SupportedProperties + { + get { throw new NotImplementedException(); } + } + + public bool Validate(string code, string tempFileName, INode currentPage, out string errorMessage) + { + throw new NotImplementedException(); + } + + public string Execute(MacroModel macro, INode currentPage) + { + throw new NotImplementedException(); + } + } + + public class MacroEngine2 : IMacroEngine + { + public string Name + { + get { return "MacroEngine2"; } + } + + public IEnumerable SupportedExtensions + { + get { return new[] { "me2" }; } + } + + public IEnumerable SupportedUIExtensions + { + get { throw new NotImplementedException(); } + } + + public Dictionary SupportedProperties + { + get { throw new NotImplementedException(); } + } + + public bool Validate(string code, string tempFileName, INode currentPage, out string errorMessage) + { + throw new NotImplementedException(); + } + + public string Execute(MacroModel macro, INode currentPage) + { + throw new NotImplementedException(); + } + } + #endregion + } } \ No newline at end of file diff --git a/src/Umbraco.Tests/HtmlHelperExtensionMethodsTests.cs b/src/Umbraco.Tests/Mvc/HtmlHelperExtensionMethodsTests.cs similarity index 93% rename from src/Umbraco.Tests/HtmlHelperExtensionMethodsTests.cs rename to src/Umbraco.Tests/Mvc/HtmlHelperExtensionMethodsTests.cs index 5efabd891e..beb51fa651 100644 --- a/src/Umbraco.Tests/HtmlHelperExtensionMethodsTests.cs +++ b/src/Umbraco.Tests/Mvc/HtmlHelperExtensionMethodsTests.cs @@ -1,33 +1,33 @@ -using System.Web.Mvc; -using NUnit.Framework; -using Umbraco.Web; - -namespace Umbraco.Tests -{ - [TestFixture] - public class HtmlHelperExtensionMethodsTests - { - [SetUp] - public virtual void Initialize() - { - //create an empty htmlHelper - _htmlHelper = new HtmlHelper(new ViewContext(), new ViewPage()); - } - - private HtmlHelper _htmlHelper; - - [Test] - public void Wrap_Simple() - { - var output = _htmlHelper.Wrap("div", "hello world"); - Assert.AreEqual("
hello world
", output.ToHtmlString()); - } - - [Test] - public void Wrap_Object_Attributes() - { - var output = _htmlHelper.Wrap("div", "hello world", new {style = "color:red;", onclick = "void();"}); - Assert.AreEqual("
hello world
", output.ToHtmlString()); - } - } +using System.Web.Mvc; +using NUnit.Framework; +using Umbraco.Web; + +namespace Umbraco.Tests.Mvc +{ + [TestFixture] + public class HtmlHelperExtensionMethodsTests + { + [SetUp] + public virtual void Initialize() + { + //create an empty htmlHelper + _htmlHelper = new HtmlHelper(new ViewContext(), new ViewPage()); + } + + private HtmlHelper _htmlHelper; + + [Test] + public void Wrap_Simple() + { + var output = _htmlHelper.Wrap("div", "hello world"); + Assert.AreEqual("
hello world
", output.ToHtmlString()); + } + + [Test] + public void Wrap_Object_Attributes() + { + var output = _htmlHelper.Wrap("div", "hello world", new {style = "color:red;", onclick = "void();"}); + Assert.AreEqual("
hello world
", output.ToHtmlString()); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Tests/PluginManagerExtensions.cs b/src/Umbraco.Tests/Plugins/PluginManagerExtensions.cs similarity index 89% rename from src/Umbraco.Tests/PluginManagerExtensions.cs rename to src/Umbraco.Tests/Plugins/PluginManagerExtensions.cs index 0daad7be44..b8ff7d20c2 100644 --- a/src/Umbraco.Tests/PluginManagerExtensions.cs +++ b/src/Umbraco.Tests/Plugins/PluginManagerExtensions.cs @@ -1,17 +1,17 @@ -using System; -using System.Collections.Generic; -using Umbraco.Core; - -namespace Umbraco.Tests -{ - /// - /// Used for PluginTypeResolverTests - /// - internal static class PluginManagerExtensions - { - public static IEnumerable ResolveFindMeTypes(this PluginManager resolver) - { - return resolver.ResolveTypes(); - } - } +using System; +using System.Collections.Generic; +using Umbraco.Core; + +namespace Umbraco.Tests.Plugins +{ + /// + /// Used for PluginTypeResolverTests + /// + internal static class PluginManagerExtensions + { + public static IEnumerable ResolveFindMeTypes(this PluginManager resolver) + { + return resolver.ResolveTypes(); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Tests/PluginManagerTests.cs b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs similarity index 96% rename from src/Umbraco.Tests/PluginManagerTests.cs rename to src/Umbraco.Tests/Plugins/PluginManagerTests.cs index 8c510256f3..aa9fc7339f 100644 --- a/src/Umbraco.Tests/PluginManagerTests.cs +++ b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs @@ -1,30 +1,26 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; -using System.Web.Compilation; using NUnit.Framework; using SqlCE4Umbraco; +using umbraco; +using umbraco.businesslogic; +using umbraco.cms.businesslogic; using Umbraco.Core; using Umbraco.Core.IO; using Umbraco.Core.PropertyEditors; +using umbraco.DataLayer; +using umbraco.editorControls; +using umbraco.MacroEngines; using Umbraco.Tests.TestHelpers; +using umbraco.uicontrols; using Umbraco.Web; using Umbraco.Web.PropertyEditors; -using umbraco; -using umbraco.DataLayer; -using umbraco.MacroEngines; -using umbraco.businesslogic; -using umbraco.cms.businesslogic; -using umbraco.editorControls; -using umbraco.interfaces; -using umbraco.uicontrols; -using umbraco.cms; -namespace Umbraco.Tests +namespace Umbraco.Tests.Plugins { [TestFixture] diff --git a/src/Umbraco.Tests/TypeFinderTests.cs b/src/Umbraco.Tests/Plugins/TypeFinderTests.cs similarity index 97% rename from src/Umbraco.Tests/TypeFinderTests.cs rename to src/Umbraco.Tests/Plugins/TypeFinderTests.cs index d9952a9ac0..8611570ef3 100644 --- a/src/Umbraco.Tests/TypeFinderTests.cs +++ b/src/Umbraco.Tests/Plugins/TypeFinderTests.cs @@ -1,641 +1,638 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Security; -using System.Text; -using System.Threading; -using System.Web; -using System.Web.Compilation; -using NUnit.Framework; -using SqlCE4Umbraco; -using Umbraco.Core; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Tests; -using Umbraco.Tests.TestHelpers; -using Umbraco.Web.BaseRest; -using umbraco; -using umbraco.DataLayer; -using umbraco.MacroEngines; -using umbraco.businesslogic; -using umbraco.cms.businesslogic; -using umbraco.editorControls.tags; -using umbraco.interfaces; -using umbraco.uicontrols; - -namespace Umbraco.Tests -{ - - /// - /// Tests for typefinder - /// - [TestFixture] - public class TypeFinderTests - { - /// - /// List of assemblies to scan - /// - private Assembly[] _assemblies; - - [SetUp] - public void Initialize() - { - TestHelper.SetupLog4NetForTests(); - - _assemblies = new[] - { - this.GetType().Assembly, - typeof(ApplicationStartupHandler).Assembly, - typeof(SqlCEHelper).Assembly, - typeof(CMSNode).Assembly, - typeof(System.Guid).Assembly, - typeof(NUnit.Framework.Assert).Assembly, - typeof(Microsoft.CSharp.CSharpCodeProvider).Assembly, - typeof(System.Xml.NameTable).Assembly, - typeof(System.Configuration.GenericEnumConverter).Assembly, - typeof(System.Web.SiteMap).Assembly, - typeof(TabPage).Assembly, - typeof(System.Web.Mvc.ActionResult).Assembly, - typeof(TypeFinder).Assembly, - typeof(ISqlHelper).Assembly, - typeof(ICultureDictionary).Assembly, - typeof(Tag).Assembly, - typeof(global::UmbracoExamine.BaseUmbracoIndexer).Assembly - }; - - } - - [Test] - public void Find_Class_Of_Type_With_Attribute() - { - - var typesFound = TypeFinder.FindClassesOfTypeWithAttribute(_assemblies); - Assert.AreEqual(2, typesFound.Count()); - } - - [Test] - public void Find_Classes_Of_Type() - { - var typesFound = TypeFinder.FindClassesOfType(_assemblies); - var originalTypesFound = TypeFinderOriginal.FindClassesOfType(_assemblies); - - Assert.AreEqual(originalTypesFound.Count(), typesFound.Count()); - Assert.AreEqual(5, typesFound.Count()); - Assert.AreEqual(5, originalTypesFound.Count()); - } - - [Test] - public void Find_Classes_With_Attribute() - { - var typesFound = TypeFinder.FindClassesWithAttribute(_assemblies); - Assert.AreEqual(1, typesFound.Count()); - } - - [Ignore] - [Test] - public void Benchmark_Original_Finder() - { - using (DisposableTimer.TraceDuration("Starting test", "Finished test")) - { - using (DisposableTimer.TraceDuration("Starting FindClassesOfType", "Finished FindClassesOfType")) - { - for (var i = 0; i < 1000; i++) - { - Assert.Greater(TypeFinderOriginal.FindClassesOfType(_assemblies).Count(), 0); - } - } - using (DisposableTimer.TraceDuration("Starting FindClassesOfTypeWithAttribute", "Finished FindClassesOfTypeWithAttribute")) - { - for (var i = 0; i < 1000; i++) - { - Assert.Greater(TypeFinderOriginal.FindClassesOfTypeWithAttribute(_assemblies).Count(), 0); - } - } - using (DisposableTimer.TraceDuration("Starting FindClassesWithAttribute", "Finished FindClassesWithAttribute")) - { - for (var i = 0; i < 1000; i++) - { - Assert.Greater(TypeFinderOriginal.FindClassesWithAttribute(_assemblies).Count(), 0); - } - } - } - - } - - [Ignore] - [Test] - public void Benchmark_New_Finder() - { - using (DisposableTimer.TraceDuration("Starting test", "Finished test")) - { - using (DisposableTimer.TraceDuration("Starting FindClassesOfType", "Finished FindClassesOfType")) - { - for (var i = 0; i < 1000; i++) - { - Assert.Greater(TypeFinder.FindClassesOfType(_assemblies).Count(), 0); - } - } - using (DisposableTimer.TraceDuration("Starting FindClassesOfTypeWithAttribute", "Finished FindClassesOfTypeWithAttribute")) - { - for (var i = 0; i < 1000; i++) - { - Assert.Greater(TypeFinder.FindClassesOfTypeWithAttribute(_assemblies).Count(), 0); - } - } - using (DisposableTimer.TraceDuration("Starting FindClassesWithAttribute", "Finished FindClassesWithAttribute")) - { - for (var i = 0; i < 1000; i++) - { - Assert.Greater(TypeFinder.FindClassesWithAttribute(_assemblies).Count(), 0); - } - } - } - - } - - public class MyTag : ITag - { - public int Id { get; private set; } - public string TagCaption { get; private set; } - public string Group { get; private set; } - } - - public class MySuperTag : MyTag - { - - } - - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class MyTestAttribute : Attribute - { - - } - - public abstract class TestEditor - { - - } - - [MyTestAttribute] - public class BenchmarkTestEditor : TestEditor - { - - } - - [MyTestAttribute] - public class MyOtherTestEditor : TestEditor - { - - } - - //USED FOR THE ABOVE TESTS - // see this issue for details: http://issues.umbraco.org/issue/U4-1187 - internal static class TypeFinderOriginal - { - - private static readonly ConcurrentBag LocalFilteredAssemblyCache = new ConcurrentBag(); - private static readonly ReaderWriterLockSlim LocalFilteredAssemblyCacheLocker = new ReaderWriterLockSlim(); - private static ReadOnlyCollection _allAssemblies = null; - private static ReadOnlyCollection _binFolderAssemblies = null; - private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); - - /// - /// lazily load a reference to all assemblies and only local assemblies. - /// This is a modified version of: http://www.dominicpettifer.co.uk/Blog/44/how-to-get-a-reference-to-all-assemblies-in-the--bin-folder - /// - /// - /// We do this because we cannot use AppDomain.Current.GetAssemblies() as this will return only assemblies that have been - /// loaded in the CLR, not all assemblies. - /// See these threads: - /// http://issues.umbraco.org/issue/U5-198 - /// http://stackoverflow.com/questions/3552223/asp-net-appdomain-currentdomain-getassemblies-assemblies-missing-after-app - /// http://stackoverflow.com/questions/2477787/difference-between-appdomain-getassemblies-and-buildmanager-getreferencedassembl - /// - internal static IEnumerable GetAllAssemblies() - { - if (_allAssemblies == null) - { - using (new WriteLock(Locker)) - { - List assemblies = null; - try - { - var isHosted = HttpContext.Current != null; - - try - { - if (isHosted) - { - assemblies = new List(BuildManager.GetReferencedAssemblies().Cast()); - } - } - catch (InvalidOperationException e) - { - if (!(e.InnerException is SecurityException)) - throw; - } - - - if (assemblies == null) - { - //NOTE: we cannot use AppDomain.CurrentDomain.GetAssemblies() because this only returns assemblies that have - // already been loaded in to the app domain, instead we will look directly into the bin folder and load each one. - var binFolder = Assembly.GetExecutingAssembly().GetAssemblyFile().Directory; - var binAssemblyFiles = Directory.GetFiles(binFolder.FullName, "*.dll", SearchOption.TopDirectoryOnly).ToList(); - assemblies = new List(); - foreach (var a in binAssemblyFiles) - { - try - { - var assName = AssemblyName.GetAssemblyName(a); - var ass = Assembly.Load(assName); - assemblies.Add(ass); - } - catch (Exception e) - { - if (e is SecurityException || e is BadImageFormatException) - { - //swallow these exceptions - } - else - { - throw; - } - } - } - } - - //if for some reason they are still no assemblies, then use the AppDomain to load in already loaded assemblies. - if (!assemblies.Any()) - { - assemblies.AddRange(AppDomain.CurrentDomain.GetAssemblies().ToList()); - } - - //here we are trying to get the App_Code assembly - var fileExtensions = new[] { ".cs", ".vb" }; //only vb and cs files are supported - var appCodeFolder = new DirectoryInfo(IOHelper.MapPath(IOHelper.ResolveUrl("~/App_code"))); - //check if the folder exists and if there are any files in it with the supported file extensions - if (appCodeFolder.Exists && (fileExtensions.Any(x => appCodeFolder.GetFiles("*" + x).Any()))) - { - var appCodeAssembly = Assembly.Load("App_Code"); - if (!assemblies.Contains(appCodeAssembly)) // BuildManager will find App_Code already - assemblies.Add(appCodeAssembly); - } - - //now set the _allAssemblies - _allAssemblies = new ReadOnlyCollection(assemblies); - - } - catch (InvalidOperationException e) - { - if (!(e.InnerException is SecurityException)) - throw; - - _binFolderAssemblies = _allAssemblies; - } - } - } - - return _allAssemblies; - } - - /// - /// Returns only assemblies found in the bin folder that have been loaded into the app domain. - /// - /// - /// - /// This will be used if we implement App_Plugins from Umbraco v5 but currently it is not used. - /// - internal static IEnumerable GetBinAssemblies() - { - - if (_binFolderAssemblies == null) - { - using (new WriteLock(Locker)) - { - var assemblies = GetAssembliesWithKnownExclusions().ToArray(); - var binFolder = Assembly.GetExecutingAssembly().GetAssemblyFile().Directory; - var binAssemblyFiles = Directory.GetFiles(binFolder.FullName, "*.dll", SearchOption.TopDirectoryOnly).ToList(); - var domainAssemblyNames = binAssemblyFiles.Select(AssemblyName.GetAssemblyName); - var safeDomainAssemblies = new List(); - var binFolderAssemblies = new List(); - - foreach (var a in assemblies) - { - try - { - //do a test to see if its queryable in med trust - var assemblyFile = a.GetAssemblyFile(); - safeDomainAssemblies.Add(a); - } - catch (SecurityException) - { - //we will just ignore this because this will fail - //in medium trust for system assemblies, we get an exception but we just want to continue until we get to - //an assembly that is ok. - } - } - - foreach (var assemblyName in domainAssemblyNames) - { - try - { - var foundAssembly = safeDomainAssemblies.FirstOrDefault(a => a.GetAssemblyFile() == assemblyName.GetAssemblyFile()); - if (foundAssembly != null) - { - binFolderAssemblies.Add(foundAssembly); - } - } - catch (SecurityException) - { - //we will just ignore this because if we are trying to do a call to: - // AssemblyName.ReferenceMatchesDefinition(a.GetName(), assemblyName))) - //in medium trust for system assemblies, we get an exception but we just want to continue until we get to - //an assembly that is ok. - } - } - - _binFolderAssemblies = new ReadOnlyCollection(binFolderAssemblies); - } - } - return _binFolderAssemblies; - } - - /// - /// Return a list of found local Assemblies excluding the known assemblies we don't want to scan - /// and exluding the ones passed in and excluding the exclusion list filter, the results of this are - /// cached for perforance reasons. - /// - /// - /// - internal static IEnumerable GetAssembliesWithKnownExclusions( - IEnumerable excludeFromResults = null) - { - if (LocalFilteredAssemblyCache.Any()) return LocalFilteredAssemblyCache; - using (new WriteLock(LocalFilteredAssemblyCacheLocker)) - { - var assemblies = GetFilteredAssemblies(excludeFromResults, KnownAssemblyExclusionFilter); - assemblies.ForEach(LocalFilteredAssemblyCache.Add); - } - return LocalFilteredAssemblyCache; - } - - /// - /// Return a list of found local Assemblies and exluding the ones passed in and excluding the exclusion list filter - /// - /// - /// - /// - private static IEnumerable GetFilteredAssemblies( - IEnumerable excludeFromResults = null, - string[] exclusionFilter = null) - { - if (excludeFromResults == null) - excludeFromResults = new List(); - if (exclusionFilter == null) - exclusionFilter = new string[] { }; - - return GetAllAssemblies() - .Where(x => !excludeFromResults.Contains(x) - && !x.GlobalAssemblyCache - && !exclusionFilter.Any(f => x.FullName.StartsWith(f))); - } - - /// - /// this is our assembly filter to filter out known types that def dont contain types we'd like to find or plugins - /// - /// - /// NOTE the comma vs period... comma delimits the name in an Assembly FullName property so if it ends with comma then its an exact name match - /// - internal static readonly string[] KnownAssemblyExclusionFilter = new[] - { - "mscorlib,", - "System.", - "Antlr3.", - "Autofac.", - "Autofac,", - "Castle.", - "ClientDependency.", - "DataAnnotationsExtensions.", - "DataAnnotationsExtensions,", - "Dynamic,", - "HtmlDiff,", - "Iesi.Collections,", - "log4net,", - "Microsoft.", - "Newtonsoft.", - "NHibernate.", - "NHibernate,", - "NuGet.", - "RouteDebugger,", - "SqlCE4Umbraco,", - "umbraco.datalayer,", - "umbraco.interfaces,", - "umbraco.providers,", - "Umbraco.Web.UI,", - "umbraco.webservices", - "Lucene.", - "Examine,", - "Examine.", - "ServiceStack.", - "MySql.", - "HtmlAgilityPack.", - "TidyNet.", - "ICSharpCode.", - "CookComputing.", - /* Mono */ - "MonoDevelop.NUnit" - }; - - public static IEnumerable FindClassesOfTypeWithAttribute() - where TAttribute : Attribute - { - return FindClassesOfTypeWithAttribute(GetAssembliesWithKnownExclusions(), true); - } - - public static IEnumerable FindClassesOfTypeWithAttribute(IEnumerable assemblies) - where TAttribute : Attribute - { - return FindClassesOfTypeWithAttribute(assemblies, true); - } - - public static IEnumerable FindClassesOfTypeWithAttribute(IEnumerable assemblies, bool onlyConcreteClasses) - where TAttribute : Attribute - { - if (assemblies == null) throw new ArgumentNullException("assemblies"); - - var l = new List(); - foreach (var a in assemblies) - { - var types = from t in GetTypesWithFormattedException(a) - where !t.IsInterface - && typeof(T).IsAssignableFrom(t) - && t.GetCustomAttributes(false).Any() - && (!onlyConcreteClasses || (t.IsClass && !t.IsAbstract)) - select t; - l.AddRange(types); - } - - return l; - } - - /// - /// Searches all filtered local assemblies specified for classes of the type passed in. - /// - /// - /// - public static IEnumerable FindClassesOfType() - { - return FindClassesOfType(GetAssembliesWithKnownExclusions(), true); - } - - /// - /// Returns all types found of in the assemblies specified of type T - /// - /// - /// - /// - /// - public static IEnumerable FindClassesOfType(IEnumerable assemblies, bool onlyConcreteClasses) - { - if (assemblies == null) throw new ArgumentNullException("assemblies"); - - return GetAssignablesFromType(assemblies, onlyConcreteClasses); - } - - /// - /// Returns all types found of in the assemblies specified of type T - /// - /// - /// - /// - public static IEnumerable FindClassesOfType(IEnumerable assemblies) - { - return FindClassesOfType(assemblies, true); - } - - /// - /// Finds the classes with attribute. - /// - /// - /// The assemblies. - /// if set to true only concrete classes. - /// - public static IEnumerable FindClassesWithAttribute(IEnumerable assemblies, bool onlyConcreteClasses) - where T : Attribute - { - return FindClassesWithAttribute(typeof(T), assemblies, onlyConcreteClasses); - } - - /// - /// Finds the classes with attribute. - /// - /// The attribute type - /// The assemblies. - /// if set to true only concrete classes. - /// - public static IEnumerable FindClassesWithAttribute(Type type, IEnumerable assemblies, bool onlyConcreteClasses) - { - if (assemblies == null) throw new ArgumentNullException("assemblies"); - if (!TypeHelper.IsTypeAssignableFrom(type)) - throw new ArgumentException("The type specified: " + type + " is not an Attribute type"); - - var l = new List(); - foreach (var a in assemblies) - { - var types = from t in GetTypesWithFormattedException(a) - where !t.IsInterface && t.GetCustomAttributes(type, false).Any() && (!onlyConcreteClasses || (t.IsClass && !t.IsAbstract)) - select t; - l.AddRange(types); - } - - return l; - } - - /// - /// Finds the classes with attribute. - /// - /// - /// The assemblies. - /// - public static IEnumerable FindClassesWithAttribute(IEnumerable assemblies) - where T : Attribute - { - return FindClassesWithAttribute(assemblies, true); - } - - /// - /// Finds the classes with attribute in filtered local assemblies - /// - /// - /// - public static IEnumerable FindClassesWithAttribute() - where T : Attribute - { - return FindClassesWithAttribute(GetAssembliesWithKnownExclusions()); - } - - - #region Private methods - - /// - /// Gets a collection of assignables of type T from a collection of assemblies - /// - /// - /// - /// - /// - private static IEnumerable GetAssignablesFromType(IEnumerable assemblies, bool onlyConcreteClasses) - { - return GetTypes(typeof(T), assemblies, onlyConcreteClasses); - } - - private static IEnumerable GetTypes(Type assignTypeFrom, IEnumerable assemblies, bool onlyConcreteClasses) - { - var l = new List(); - foreach (var a in assemblies) - { - var types = from t in GetTypesWithFormattedException(a) - where !t.IsInterface && assignTypeFrom.IsAssignableFrom(t) && (!onlyConcreteClasses || (t.IsClass && !t.IsAbstract)) - select t; - l.AddRange(types); - } - return l; - } - - private static IEnumerable GetTypesWithFormattedException(Assembly a) - { - //if the assembly is dynamic, do not try to scan it - if (a.IsDynamic) - return Enumerable.Empty(); - - try - { - return a.GetExportedTypes(); - } - catch (ReflectionTypeLoadException ex) - { - var sb = new StringBuilder(); - sb.AppendLine("Could not load types from assembly " + a.FullName + ", errors:"); - foreach (var loaderException in ex.LoaderExceptions.WhereNotNull()) - { - sb.AppendLine("Exception: " + loaderException.ToString()); - } - throw new ReflectionTypeLoadException(ex.Types, ex.LoaderExceptions, sb.ToString()); - } - } - - #endregion - - - - } - } - - +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security; +using System.Text; +using System.Threading; +using System.Web; +using System.Web.Compilation; +using NUnit.Framework; +using SqlCE4Umbraco; +using umbraco; +using umbraco.businesslogic; +using umbraco.cms.businesslogic; +using Umbraco.Core; +using Umbraco.Core.IO; +using umbraco.DataLayer; +using umbraco.editorControls.tags; +using umbraco.interfaces; +using umbraco.MacroEngines; +using Umbraco.Tests.TestHelpers; +using umbraco.uicontrols; +using Umbraco.Web.BaseRest; + +namespace Umbraco.Tests.Plugins +{ + + /// + /// Tests for typefinder + /// + [TestFixture] + public class TypeFinderTests + { + /// + /// List of assemblies to scan + /// + private Assembly[] _assemblies; + + [SetUp] + public void Initialize() + { + TestHelper.SetupLog4NetForTests(); + + _assemblies = new[] + { + this.GetType().Assembly, + typeof(ApplicationStartupHandler).Assembly, + typeof(SqlCEHelper).Assembly, + typeof(CMSNode).Assembly, + typeof(System.Guid).Assembly, + typeof(NUnit.Framework.Assert).Assembly, + typeof(Microsoft.CSharp.CSharpCodeProvider).Assembly, + typeof(System.Xml.NameTable).Assembly, + typeof(System.Configuration.GenericEnumConverter).Assembly, + typeof(System.Web.SiteMap).Assembly, + typeof(TabPage).Assembly, + typeof(System.Web.Mvc.ActionResult).Assembly, + typeof(TypeFinder).Assembly, + typeof(ISqlHelper).Assembly, + typeof(ICultureDictionary).Assembly, + typeof(Tag).Assembly, + typeof(global::UmbracoExamine.BaseUmbracoIndexer).Assembly + }; + + } + + [Test] + public void Find_Class_Of_Type_With_Attribute() + { + + var typesFound = TypeFinder.FindClassesOfTypeWithAttribute(_assemblies); + Assert.AreEqual(2, typesFound.Count()); + } + + [Test] + public void Find_Classes_Of_Type() + { + var typesFound = TypeFinder.FindClassesOfType(_assemblies); + var originalTypesFound = TypeFinderOriginal.FindClassesOfType(_assemblies); + + Assert.AreEqual(originalTypesFound.Count(), typesFound.Count()); + Assert.AreEqual(5, typesFound.Count()); + Assert.AreEqual(5, originalTypesFound.Count()); + } + + [Test] + public void Find_Classes_With_Attribute() + { + var typesFound = TypeFinder.FindClassesWithAttribute(_assemblies); + Assert.AreEqual(1, typesFound.Count()); + } + + [Ignore] + [Test] + public void Benchmark_Original_Finder() + { + using (DisposableTimer.TraceDuration("Starting test", "Finished test")) + { + using (DisposableTimer.TraceDuration("Starting FindClassesOfType", "Finished FindClassesOfType")) + { + for (var i = 0; i < 1000; i++) + { + Assert.Greater(TypeFinderOriginal.FindClassesOfType(_assemblies).Count(), 0); + } + } + using (DisposableTimer.TraceDuration("Starting FindClassesOfTypeWithAttribute", "Finished FindClassesOfTypeWithAttribute")) + { + for (var i = 0; i < 1000; i++) + { + Assert.Greater(TypeFinderOriginal.FindClassesOfTypeWithAttribute(_assemblies).Count(), 0); + } + } + using (DisposableTimer.TraceDuration("Starting FindClassesWithAttribute", "Finished FindClassesWithAttribute")) + { + for (var i = 0; i < 1000; i++) + { + Assert.Greater(TypeFinderOriginal.FindClassesWithAttribute(_assemblies).Count(), 0); + } + } + } + + } + + [Ignore] + [Test] + public void Benchmark_New_Finder() + { + using (DisposableTimer.TraceDuration("Starting test", "Finished test")) + { + using (DisposableTimer.TraceDuration("Starting FindClassesOfType", "Finished FindClassesOfType")) + { + for (var i = 0; i < 1000; i++) + { + Assert.Greater(TypeFinder.FindClassesOfType(_assemblies).Count(), 0); + } + } + using (DisposableTimer.TraceDuration("Starting FindClassesOfTypeWithAttribute", "Finished FindClassesOfTypeWithAttribute")) + { + for (var i = 0; i < 1000; i++) + { + Assert.Greater(TypeFinder.FindClassesOfTypeWithAttribute(_assemblies).Count(), 0); + } + } + using (DisposableTimer.TraceDuration("Starting FindClassesWithAttribute", "Finished FindClassesWithAttribute")) + { + for (var i = 0; i < 1000; i++) + { + Assert.Greater(TypeFinder.FindClassesWithAttribute(_assemblies).Count(), 0); + } + } + } + + } + + public class MyTag : ITag + { + public int Id { get; private set; } + public string TagCaption { get; private set; } + public string Group { get; private set; } + } + + public class MySuperTag : MyTag + { + + } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class MyTestAttribute : Attribute + { + + } + + public abstract class TestEditor + { + + } + + [MyTest] + public class BenchmarkTestEditor : TestEditor + { + + } + + [MyTest] + public class MyOtherTestEditor : TestEditor + { + + } + + //USED FOR THE ABOVE TESTS + // see this issue for details: http://issues.umbraco.org/issue/U4-1187 + internal static class TypeFinderOriginal + { + + private static readonly ConcurrentBag LocalFilteredAssemblyCache = new ConcurrentBag(); + private static readonly ReaderWriterLockSlim LocalFilteredAssemblyCacheLocker = new ReaderWriterLockSlim(); + private static ReadOnlyCollection _allAssemblies = null; + private static ReadOnlyCollection _binFolderAssemblies = null; + private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); + + /// + /// lazily load a reference to all assemblies and only local assemblies. + /// This is a modified version of: http://www.dominicpettifer.co.uk/Blog/44/how-to-get-a-reference-to-all-assemblies-in-the--bin-folder + /// + /// + /// We do this because we cannot use AppDomain.Current.GetAssemblies() as this will return only assemblies that have been + /// loaded in the CLR, not all assemblies. + /// See these threads: + /// http://issues.umbraco.org/issue/U5-198 + /// http://stackoverflow.com/questions/3552223/asp-net-appdomain-currentdomain-getassemblies-assemblies-missing-after-app + /// http://stackoverflow.com/questions/2477787/difference-between-appdomain-getassemblies-and-buildmanager-getreferencedassembl + /// + internal static IEnumerable GetAllAssemblies() + { + if (_allAssemblies == null) + { + using (new WriteLock(Locker)) + { + List assemblies = null; + try + { + var isHosted = HttpContext.Current != null; + + try + { + if (isHosted) + { + assemblies = new List(BuildManager.GetReferencedAssemblies().Cast()); + } + } + catch (InvalidOperationException e) + { + if (!(e.InnerException is SecurityException)) + throw; + } + + + if (assemblies == null) + { + //NOTE: we cannot use AppDomain.CurrentDomain.GetAssemblies() because this only returns assemblies that have + // already been loaded in to the app domain, instead we will look directly into the bin folder and load each one. + var binFolder = Assembly.GetExecutingAssembly().GetAssemblyFile().Directory; + var binAssemblyFiles = Directory.GetFiles(binFolder.FullName, "*.dll", SearchOption.TopDirectoryOnly).ToList(); + assemblies = new List(); + foreach (var a in binAssemblyFiles) + { + try + { + var assName = AssemblyName.GetAssemblyName(a); + var ass = Assembly.Load(assName); + assemblies.Add(ass); + } + catch (Exception e) + { + if (e is SecurityException || e is BadImageFormatException) + { + //swallow these exceptions + } + else + { + throw; + } + } + } + } + + //if for some reason they are still no assemblies, then use the AppDomain to load in already loaded assemblies. + if (!assemblies.Any()) + { + assemblies.AddRange(AppDomain.CurrentDomain.GetAssemblies().ToList()); + } + + //here we are trying to get the App_Code assembly + var fileExtensions = new[] { ".cs", ".vb" }; //only vb and cs files are supported + var appCodeFolder = new DirectoryInfo(IOHelper.MapPath(IOHelper.ResolveUrl("~/App_code"))); + //check if the folder exists and if there are any files in it with the supported file extensions + if (appCodeFolder.Exists && (fileExtensions.Any(x => appCodeFolder.GetFiles("*" + x).Any()))) + { + var appCodeAssembly = Assembly.Load("App_Code"); + if (!assemblies.Contains(appCodeAssembly)) // BuildManager will find App_Code already + assemblies.Add(appCodeAssembly); + } + + //now set the _allAssemblies + _allAssemblies = new ReadOnlyCollection(assemblies); + + } + catch (InvalidOperationException e) + { + if (!(e.InnerException is SecurityException)) + throw; + + _binFolderAssemblies = _allAssemblies; + } + } + } + + return _allAssemblies; + } + + /// + /// Returns only assemblies found in the bin folder that have been loaded into the app domain. + /// + /// + /// + /// This will be used if we implement App_Plugins from Umbraco v5 but currently it is not used. + /// + internal static IEnumerable GetBinAssemblies() + { + + if (_binFolderAssemblies == null) + { + using (new WriteLock(Locker)) + { + var assemblies = GetAssembliesWithKnownExclusions().ToArray(); + var binFolder = Assembly.GetExecutingAssembly().GetAssemblyFile().Directory; + var binAssemblyFiles = Directory.GetFiles(binFolder.FullName, "*.dll", SearchOption.TopDirectoryOnly).ToList(); + var domainAssemblyNames = binAssemblyFiles.Select(AssemblyName.GetAssemblyName); + var safeDomainAssemblies = new List(); + var binFolderAssemblies = new List(); + + foreach (var a in assemblies) + { + try + { + //do a test to see if its queryable in med trust + var assemblyFile = a.GetAssemblyFile(); + safeDomainAssemblies.Add(a); + } + catch (SecurityException) + { + //we will just ignore this because this will fail + //in medium trust for system assemblies, we get an exception but we just want to continue until we get to + //an assembly that is ok. + } + } + + foreach (var assemblyName in domainAssemblyNames) + { + try + { + var foundAssembly = safeDomainAssemblies.FirstOrDefault(a => a.GetAssemblyFile() == assemblyName.GetAssemblyFile()); + if (foundAssembly != null) + { + binFolderAssemblies.Add(foundAssembly); + } + } + catch (SecurityException) + { + //we will just ignore this because if we are trying to do a call to: + // AssemblyName.ReferenceMatchesDefinition(a.GetName(), assemblyName))) + //in medium trust for system assemblies, we get an exception but we just want to continue until we get to + //an assembly that is ok. + } + } + + _binFolderAssemblies = new ReadOnlyCollection(binFolderAssemblies); + } + } + return _binFolderAssemblies; + } + + /// + /// Return a list of found local Assemblies excluding the known assemblies we don't want to scan + /// and exluding the ones passed in and excluding the exclusion list filter, the results of this are + /// cached for perforance reasons. + /// + /// + /// + internal static IEnumerable GetAssembliesWithKnownExclusions( + IEnumerable excludeFromResults = null) + { + if (LocalFilteredAssemblyCache.Any()) return LocalFilteredAssemblyCache; + using (new WriteLock(LocalFilteredAssemblyCacheLocker)) + { + var assemblies = GetFilteredAssemblies(excludeFromResults, KnownAssemblyExclusionFilter); + assemblies.ForEach(LocalFilteredAssemblyCache.Add); + } + return LocalFilteredAssemblyCache; + } + + /// + /// Return a list of found local Assemblies and exluding the ones passed in and excluding the exclusion list filter + /// + /// + /// + /// + private static IEnumerable GetFilteredAssemblies( + IEnumerable excludeFromResults = null, + string[] exclusionFilter = null) + { + if (excludeFromResults == null) + excludeFromResults = new List(); + if (exclusionFilter == null) + exclusionFilter = new string[] { }; + + return GetAllAssemblies() + .Where(x => !excludeFromResults.Contains(x) + && !x.GlobalAssemblyCache + && !exclusionFilter.Any(f => x.FullName.StartsWith(f))); + } + + /// + /// this is our assembly filter to filter out known types that def dont contain types we'd like to find or plugins + /// + /// + /// NOTE the comma vs period... comma delimits the name in an Assembly FullName property so if it ends with comma then its an exact name match + /// + internal static readonly string[] KnownAssemblyExclusionFilter = new[] + { + "mscorlib,", + "System.", + "Antlr3.", + "Autofac.", + "Autofac,", + "Castle.", + "ClientDependency.", + "DataAnnotationsExtensions.", + "DataAnnotationsExtensions,", + "Dynamic,", + "HtmlDiff,", + "Iesi.Collections,", + "log4net,", + "Microsoft.", + "Newtonsoft.", + "NHibernate.", + "NHibernate,", + "NuGet.", + "RouteDebugger,", + "SqlCE4Umbraco,", + "umbraco.datalayer,", + "umbraco.interfaces,", + "umbraco.providers,", + "Umbraco.Web.UI,", + "umbraco.webservices", + "Lucene.", + "Examine,", + "Examine.", + "ServiceStack.", + "MySql.", + "HtmlAgilityPack.", + "TidyNet.", + "ICSharpCode.", + "CookComputing.", + /* Mono */ + "MonoDevelop.NUnit" + }; + + public static IEnumerable FindClassesOfTypeWithAttribute() + where TAttribute : Attribute + { + return FindClassesOfTypeWithAttribute(GetAssembliesWithKnownExclusions(), true); + } + + public static IEnumerable FindClassesOfTypeWithAttribute(IEnumerable assemblies) + where TAttribute : Attribute + { + return FindClassesOfTypeWithAttribute(assemblies, true); + } + + public static IEnumerable FindClassesOfTypeWithAttribute(IEnumerable assemblies, bool onlyConcreteClasses) + where TAttribute : Attribute + { + if (assemblies == null) throw new ArgumentNullException("assemblies"); + + var l = new List(); + foreach (var a in assemblies) + { + var types = from t in GetTypesWithFormattedException(a) + where !t.IsInterface + && typeof(T).IsAssignableFrom(t) + && t.GetCustomAttributes(false).Any() + && (!onlyConcreteClasses || (t.IsClass && !t.IsAbstract)) + select t; + l.AddRange(types); + } + + return l; + } + + /// + /// Searches all filtered local assemblies specified for classes of the type passed in. + /// + /// + /// + public static IEnumerable FindClassesOfType() + { + return FindClassesOfType(GetAssembliesWithKnownExclusions(), true); + } + + /// + /// Returns all types found of in the assemblies specified of type T + /// + /// + /// + /// + /// + public static IEnumerable FindClassesOfType(IEnumerable assemblies, bool onlyConcreteClasses) + { + if (assemblies == null) throw new ArgumentNullException("assemblies"); + + return GetAssignablesFromType(assemblies, onlyConcreteClasses); + } + + /// + /// Returns all types found of in the assemblies specified of type T + /// + /// + /// + /// + public static IEnumerable FindClassesOfType(IEnumerable assemblies) + { + return FindClassesOfType(assemblies, true); + } + + /// + /// Finds the classes with attribute. + /// + /// + /// The assemblies. + /// if set to true only concrete classes. + /// + public static IEnumerable FindClassesWithAttribute(IEnumerable assemblies, bool onlyConcreteClasses) + where T : Attribute + { + return FindClassesWithAttribute(typeof(T), assemblies, onlyConcreteClasses); + } + + /// + /// Finds the classes with attribute. + /// + /// The attribute type + /// The assemblies. + /// if set to true only concrete classes. + /// + public static IEnumerable FindClassesWithAttribute(Type type, IEnumerable assemblies, bool onlyConcreteClasses) + { + if (assemblies == null) throw new ArgumentNullException("assemblies"); + if (!TypeHelper.IsTypeAssignableFrom(type)) + throw new ArgumentException("The type specified: " + type + " is not an Attribute type"); + + var l = new List(); + foreach (var a in assemblies) + { + var types = from t in GetTypesWithFormattedException(a) + where !t.IsInterface && t.GetCustomAttributes(type, false).Any() && (!onlyConcreteClasses || (t.IsClass && !t.IsAbstract)) + select t; + l.AddRange(types); + } + + return l; + } + + /// + /// Finds the classes with attribute. + /// + /// + /// The assemblies. + /// + public static IEnumerable FindClassesWithAttribute(IEnumerable assemblies) + where T : Attribute + { + return FindClassesWithAttribute(assemblies, true); + } + + /// + /// Finds the classes with attribute in filtered local assemblies + /// + /// + /// + public static IEnumerable FindClassesWithAttribute() + where T : Attribute + { + return FindClassesWithAttribute(GetAssembliesWithKnownExclusions()); + } + + + #region Private methods + + /// + /// Gets a collection of assignables of type T from a collection of assemblies + /// + /// + /// + /// + /// + private static IEnumerable GetAssignablesFromType(IEnumerable assemblies, bool onlyConcreteClasses) + { + return GetTypes(typeof(T), assemblies, onlyConcreteClasses); + } + + private static IEnumerable GetTypes(Type assignTypeFrom, IEnumerable assemblies, bool onlyConcreteClasses) + { + var l = new List(); + foreach (var a in assemblies) + { + var types = from t in GetTypesWithFormattedException(a) + where !t.IsInterface && assignTypeFrom.IsAssignableFrom(t) && (!onlyConcreteClasses || (t.IsClass && !t.IsAbstract)) + select t; + l.AddRange(types); + } + return l; + } + + private static IEnumerable GetTypesWithFormattedException(Assembly a) + { + //if the assembly is dynamic, do not try to scan it + if (a.IsDynamic) + return Enumerable.Empty(); + + try + { + return a.GetExportedTypes(); + } + catch (ReflectionTypeLoadException ex) + { + var sb = new StringBuilder(); + sb.AppendLine("Could not load types from assembly " + a.FullName + ", errors:"); + foreach (var loaderException in ex.LoaderExceptions.WhereNotNull()) + { + sb.AppendLine("Exception: " + loaderException.ToString()); + } + throw new ReflectionTypeLoadException(ex.Types, ex.LoaderExceptions, sb.ToString()); + } + } + + #endregion + + + + } + } + + } \ No newline at end of file diff --git a/src/Umbraco.Tests/TypeHelperTests.cs b/src/Umbraco.Tests/Plugins/TypeHelperTests.cs similarity index 90% rename from src/Umbraco.Tests/TypeHelperTests.cs rename to src/Umbraco.Tests/Plugins/TypeHelperTests.cs index 9b8d38aec7..404dc00eaa 100644 --- a/src/Umbraco.Tests/TypeHelperTests.cs +++ b/src/Umbraco.Tests/Plugins/TypeHelperTests.cs @@ -1,79 +1,72 @@ -using System; -using System.ComponentModel; -using System.Data.Common; -using System.Data.Odbc; -using System.Data.OleDb; -using System.Data.SqlClient; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Web; -using Umbraco.Web.Cache; -using Umbraco.Web.Scheduling; -using UmbracoExamine; -using UmbracoExamine.DataServices; -using umbraco; -using umbraco.presentation; -using umbraco.presentation.nodeFactory; -using umbraco.presentation.umbraco.Search; - -namespace Umbraco.Tests -{ - /// - /// Tests for TypeHelper - /// - [TestFixture] - public class TypeHelperTests - { - - [Test] - public void Is_Static_Class() - { - Assert.IsTrue(TypeHelper.IsStaticClass(typeof(TypeHelper))); - Assert.IsFalse(TypeHelper.IsStaticClass(typeof(TypeHelperTests))); - } - - [Test] - public void Find_Common_Base_Class() - { - var t1 = TypeHelper.GetLowestBaseType(typeof (OleDbCommand), - typeof (OdbcCommand), - typeof (SqlCommand)); - Assert.IsFalse(t1.Success); - - var t2 = TypeHelper.GetLowestBaseType(typeof (OleDbCommand), - typeof (OdbcCommand), - typeof (SqlCommand), - typeof (Component)); - Assert.IsTrue(t2.Success); - Assert.AreEqual(typeof(Component), t2.Result); - - var t3 = TypeHelper.GetLowestBaseType(typeof (OleDbCommand), - typeof (OdbcCommand), - typeof (SqlCommand), - typeof (Component), - typeof (Component).BaseType); - Assert.IsTrue(t3.Success); - Assert.AreEqual(typeof(MarshalByRefObject), t3.Result); - - var t4 = TypeHelper.GetLowestBaseType(typeof(OleDbCommand), - typeof(OdbcCommand), - typeof(SqlCommand), - typeof(Component), - typeof(Component).BaseType, - typeof(int)); - Assert.IsFalse(t4.Success); - - var t5 = TypeHelper.GetLowestBaseType(typeof(PropertyAliasDto)); - Assert.IsTrue(t5.Success); - Assert.AreEqual(typeof(PropertyAliasDto), t5.Result); - - var t6 = TypeHelper.GetLowestBaseType(typeof (IApplicationEventHandler), - typeof (Scheduler), - typeof(CacheRefresherEventHandler)); - Assert.IsTrue(t6.Success); - Assert.AreEqual(typeof(IApplicationEventHandler), t6.Result); - - } - - } +using System; +using System.ComponentModel; +using System.Data.Odbc; +using System.Data.OleDb; +using System.Data.SqlClient; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Web.Cache; +using Umbraco.Web.Scheduling; +using UmbracoExamine.DataServices; + +namespace Umbraco.Tests.Plugins +{ + /// + /// Tests for TypeHelper + /// + [TestFixture] + public class TypeHelperTests + { + + [Test] + public void Is_Static_Class() + { + Assert.IsTrue(TypeHelper.IsStaticClass(typeof(TypeHelper))); + Assert.IsFalse(TypeHelper.IsStaticClass(typeof(TypeHelperTests))); + } + + [Test] + public void Find_Common_Base_Class() + { + var t1 = TypeHelper.GetLowestBaseType(typeof (OleDbCommand), + typeof (OdbcCommand), + typeof (SqlCommand)); + Assert.IsFalse(t1.Success); + + var t2 = TypeHelper.GetLowestBaseType(typeof (OleDbCommand), + typeof (OdbcCommand), + typeof (SqlCommand), + typeof (Component)); + Assert.IsTrue(t2.Success); + Assert.AreEqual(typeof(Component), t2.Result); + + var t3 = TypeHelper.GetLowestBaseType(typeof (OleDbCommand), + typeof (OdbcCommand), + typeof (SqlCommand), + typeof (Component), + typeof (Component).BaseType); + Assert.IsTrue(t3.Success); + Assert.AreEqual(typeof(MarshalByRefObject), t3.Result); + + var t4 = TypeHelper.GetLowestBaseType(typeof(OleDbCommand), + typeof(OdbcCommand), + typeof(SqlCommand), + typeof(Component), + typeof(Component).BaseType, + typeof(int)); + Assert.IsFalse(t4.Success); + + var t5 = TypeHelper.GetLowestBaseType(typeof(PropertyAliasDto)); + Assert.IsTrue(t5.Success); + Assert.AreEqual(typeof(PropertyAliasDto), t5.Result); + + var t6 = TypeHelper.GetLowestBaseType(typeof (IApplicationEventHandler), + typeof (Scheduler), + typeof(CacheRefresherEventHandler)); + Assert.IsTrue(t6.Success); + Assert.AreEqual(typeof(IApplicationEventHandler), t6.Result); + + } + + } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs new file mode 100644 index 0000000000..8162b27c62 --- /dev/null +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Web.Scheduling; + +namespace Umbraco.Tests.Scheduling +{ + [TestFixture] + public class BackgroundTaskRunnerTests + { + + + [Test] + public void Startup_And_Shutdown() + { + BackgroundTaskRunner tManager; + using (tManager = new BackgroundTaskRunner(true, true)) + { + tManager.StartUp(); + } + + NUnit.Framework.Assert.IsFalse(tManager.IsRunning); + } + + [Test] + public void Startup_Starts_Automatically() + { + BackgroundTaskRunner tManager; + using (tManager = new BackgroundTaskRunner(true, true)) + { + tManager.Add(new MyTask()); + NUnit.Framework.Assert.IsTrue(tManager.IsRunning); + } + } + + [Test] + public void Task_Runs() + { + var myTask = new MyTask(); + var waitHandle = new ManualResetEvent(false); + BackgroundTaskRunner tManager; + using (tManager = new BackgroundTaskRunner(true, true)) + { + tManager.TaskCompleted += (sender, task) => waitHandle.Set(); + + tManager.Add(myTask); + + //wait for ITasks to complete + waitHandle.WaitOne(); + + NUnit.Framework.Assert.IsTrue(myTask.Ended != default(DateTime)); + } + } + + [Test] + public void Many_Tasks_Run() + { + var tasks = new Dictionary(); + for (var i = 0; i < 10; i++) + { + tasks.Add(new MyTask(), new ManualResetEvent(false)); + } + + BackgroundTaskRunner tManager; + using (tManager = new BackgroundTaskRunner(true, true)) + { + tManager.TaskCompleted += (sender, task) => tasks[task.Task].Set(); + + tasks.ForEach(t => tManager.Add(t.Key)); + + //wait for all ITasks to complete + WaitHandle.WaitAll(tasks.Values.Select(x => (WaitHandle)x).ToArray()); + + foreach (var task in tasks) + { + NUnit.Framework.Assert.IsTrue(task.Key.Ended != default(DateTime)); + } + } + } + + [Test] + public void Tasks_Can_Keep_Being_Added_And_Will_Execute() + { + Func> getTasks = () => + { + var newTasks = new Dictionary(); + for (var i = 0; i < 10; i++) + { + newTasks.Add(new MyTask(), new ManualResetEvent(false)); + } + return newTasks; + }; + + IDictionary tasks = getTasks(); + + BackgroundTaskRunner tManager; + using (tManager = new BackgroundTaskRunner(true, true)) + { + tManager.TaskCompleted += (sender, task) => tasks[task.Task].Set(); + + //execute first batch + tasks.ForEach(t => tManager.Add(t.Key)); + + //wait for all ITasks to complete + WaitHandle.WaitAll(tasks.Values.Select(x => (WaitHandle)x).ToArray()); + + foreach (var task in tasks) + { + NUnit.Framework.Assert.IsTrue(task.Key.Ended != default(DateTime)); + } + + //execute another batch after a bit + Thread.Sleep(2000); + + tasks = getTasks(); + tasks.ForEach(t => tManager.Add(t.Key)); + + //wait for all ITasks to complete + WaitHandle.WaitAll(tasks.Values.Select(x => (WaitHandle)x).ToArray()); + + foreach (var task in tasks) + { + NUnit.Framework.Assert.IsTrue(task.Key.Ended != default(DateTime)); + } + } + } + + [Test] + public void Task_Queue_Will_Be_Completed_Before_Shutdown() + { + var tasks = new Dictionary(); + for (var i = 0; i < 10; i++) + { + tasks.Add(new MyTask(), new ManualResetEvent(false)); + } + + BackgroundTaskRunner tManager; + using (tManager = new BackgroundTaskRunner(true, true)) + { + tManager.TaskCompleted += (sender, task) => tasks[task.Task].Set(); + + tasks.ForEach(t => tManager.Add(t.Key)); + + ////wait for all ITasks to complete + //WaitHandle.WaitAll(tasks.Values.Select(x => (WaitHandle)x).ToArray()); + + tManager.Stop(false); + //immediate stop will block until complete - but since we are running on + // a single thread this doesn't really matter as the above will just process + // until complete. + tManager.Stop(true); + + NUnit.Framework.Assert.AreEqual(0, tManager.TaskCount); + } + } + + //NOTE: These tests work in .Net 4.5 but in this current version we don't have the correct + // async/await signatures with GetAwaiter, so am just commenting these out in this version + + [Test] + public async void Non_Persistent_Runner_Will_End_After_Queue_Empty() + { + var tasks = new List(); + for (var i = 0; i < 10; i++) + { + tasks.Add(new MyTask()); + } + + BackgroundTaskRunner tManager; + using (tManager = new BackgroundTaskRunner(persistentThread: false, dedicatedThread:true)) + { + tasks.ForEach(t => tManager.Add(t)); + + //wait till the thread is done + await tManager; + + foreach (var task in tasks) + { + Assert.IsTrue(task.Ended != default(DateTime)); + } + + Assert.AreEqual(TaskStatus.RanToCompletion, tManager.TaskStatus); + Assert.IsFalse(tManager.IsRunning); + Assert.IsFalse(tManager.IsDisposed); + } + } + + [Test] + public async void Non_Persistent_Runner_Will_Start_New_Threads_When_Required() + { + Func> getTasks = () => + { + var newTasks = new List(); + for (var i = 0; i < 10; i++) + { + newTasks.Add(new MyTask()); + } + return newTasks; + }; + + List tasks = getTasks(); + + BackgroundTaskRunner tManager; + using (tManager = new BackgroundTaskRunner(persistentThread: false, dedicatedThread: true)) + { + tasks.ForEach(t => tManager.Add(t)); + + //wait till the thread is done + await tManager; + + Assert.AreEqual(TaskStatus.RanToCompletion, tManager.TaskStatus); + Assert.IsFalse(tManager.IsRunning); + Assert.IsFalse(tManager.IsDisposed); + + foreach (var task in tasks) + { + Assert.IsTrue(task.Ended != default(DateTime)); + } + + //create more tasks + tasks = getTasks(); + + //add more tasks + tasks.ForEach(t => tManager.Add(t)); + + //wait till the thread is done + await tManager; + + foreach (var task in tasks) + { + Assert.IsTrue(task.Ended != default(DateTime)); + } + + Assert.AreEqual(TaskStatus.RanToCompletion, tManager.TaskStatus); + Assert.IsFalse(tManager.IsRunning); + Assert.IsFalse(tManager.IsDisposed); + } + } + + private class MyTask : BaseTask + { + public MyTask() + { + } + + public override void Run() + { + Thread.Sleep(500); + } + + public override void Cancel() + { + + } + } + + public abstract class BaseTask : IBackgroundTask + { + public Guid UniqueId { get; protected set; } + + public abstract void Run(); + public abstract void Cancel(); + + public DateTime Queued { get; set; } + public DateTime Started { get; set; } + public DateTime Ended { get; set; } + + public virtual void Dispose() + { + Ended = DateTime.Now; + } + } + + } +} diff --git a/src/Umbraco.Tests/ServerEnvironmentHelperTests.cs b/src/Umbraco.Tests/ServerEnvironmentHelperTests.cs new file mode 100644 index 0000000000..237c6cb17d --- /dev/null +++ b/src/Umbraco.Tests/ServerEnvironmentHelperTests.cs @@ -0,0 +1,92 @@ +using System.Configuration; +using System.Linq; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Sync; + +namespace Umbraco.Tests +{ + [TestFixture] + public class ServerEnvironmentHelperTests + { + [Test] + public void Get_Base_Url_Single_Server_Orig_Request_Url_No_SSL() + { + var appContext = new ApplicationContext(null) + { + OriginalRequestUrl = "test.com" + }; + + ConfigurationManager.AppSettings.Set("umbracoUseSSL", "false"); + + var result = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl( + appContext, + Mock.Of( + section => + section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) + && section.ScheduledTasks == Mock.Of())); + + + Assert.AreEqual("http://test.com/", result); + } + + [Test] + public void Get_Base_Url_Single_Server_Orig_Request_Url_With_SSL() + { + var appContext = new ApplicationContext(null) + { + OriginalRequestUrl = "test.com" + }; + + ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); + + var result = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl( + appContext, + Mock.Of( + section => + section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) + && section.ScheduledTasks == Mock.Of())); + + + Assert.AreEqual("https://test.com/", result); + } + + [Test] + public void Get_Base_Url_Single_Server_Via_Config_Url_No_SSL() + { + var appContext = new ApplicationContext(null); + + ConfigurationManager.AppSettings.Set("umbracoUseSSL", "false"); + + var result = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl( + appContext, + Mock.Of( + section => + section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) + && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == "mycoolhost.com/hello/world"))); + + + Assert.AreEqual("http://mycoolhost.com/hello/world/", result); + } + + [Test] + public void Get_Base_Url_Single_Server_Via_Config_Url_With_SSL() + { + var appContext = new ApplicationContext(null); + + ConfigurationManager.AppSettings.Set("umbracoUseSSL", "true"); + + var result = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl( + appContext, + Mock.Of( + section => + section.DistributedCall == Mock.Of(callSection => callSection.Servers == Enumerable.Empty()) + && section.ScheduledTasks == Mock.Of(tasksSection => tasksSection.BaseUrl == "mycoolhost.com/hello/world"))); + + + Assert.AreEqual("https://mycoolhost.com/hello/world/", result); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Trees/BaseContentTreeTests.cs b/src/Umbraco.Tests/TreesAndSections/BaseContentTreeTests.cs similarity index 92% rename from src/Umbraco.Tests/Trees/BaseContentTreeTests.cs rename to src/Umbraco.Tests/TreesAndSections/BaseContentTreeTests.cs index 63a9f9f14d..57a974fc95 100644 --- a/src/Umbraco.Tests/Trees/BaseContentTreeTests.cs +++ b/src/Umbraco.Tests/TreesAndSections/BaseContentTreeTests.cs @@ -1,127 +1,123 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using NUnit.Framework; -using umbraco.cms.presentation.Trees; - -namespace Umbraco.Tests.Trees -{ - [TestFixture] - public class BaseContentTreeTests - { - - [TearDown] - public void TestTearDown() - { - BaseTree.AfterTreeRender -= EventHandler; - BaseTree.BeforeTreeRender -= EventHandler; - } - - [Test] - public void Run_Optimized() - { - var tree1 = new MyOptimizedContentTree1("content"); - var tree2 = new MyOptimizedContentTree2("content"); - - Assert.IsTrue(tree1.UseOptimizedRendering); - Assert.IsTrue(tree2.UseOptimizedRendering); - } - - [Test] - public void Not_Optimized_Events_AfterRender() - { - var tree = new MyOptimizedContentTree1("content"); - - BaseTree.AfterTreeRender += EventHandler; - - Assert.IsFalse(tree.UseOptimizedRendering); - } - - [Test] - public void Not_Optimized_Events_BeforeRender() - { - var tree = new MyOptimizedContentTree1("content"); - - BaseTree.BeforeTreeRender += EventHandler; - - Assert.IsFalse(tree.UseOptimizedRendering); - } - - [Test] - public void Not_Optimized_Overriden_Method() - { - var tree = new MyNotOptimizedContentTree("content"); - - Assert.IsFalse(tree.UseOptimizedRendering); - } - - private void EventHandler(object sender, TreeEventArgs treeEventArgs) - { - - } - - //optimized because we are not overriding OnRenderNode - public class MyOptimizedContentTree1 : BaseContentTree - { - public MyOptimizedContentTree1(string application) - : base(application) - { - } - - protected override void CreateRootNode(ref XmlTreeNode rootNode) - { - - } - } - - public class MyOptimizedContentTree2 : BaseContentTree - { - public MyOptimizedContentTree2(string application) - : base(application) - { - } - - protected override bool LoadMinimalDocument - { - get { return true; } - } - - protected override void CreateRootNode(ref XmlTreeNode rootNode) - { - - } - - //even if we override it will still be optimized because of the LoadMinimalDocument flag - protected override void OnRenderNode(ref XmlTreeNode xNode, umbraco.cms.businesslogic.web.Document doc) - { - base.OnRenderNode(ref xNode, doc); - } - } - - public class MyNotOptimizedContentTree : BaseContentTree - { - public MyNotOptimizedContentTree(string application) - : base(application) - { - } - - protected override void CreateRootNode(ref XmlTreeNode rootNode) - { - - } - - protected override bool LoadMinimalDocument - { - get { return false; } - } - - protected override void OnRenderNode(ref XmlTreeNode xNode, umbraco.cms.businesslogic.web.Document doc) - { - base.OnRenderNode(ref xNode, doc); - } - } - - - } -} +using NUnit.Framework; +using umbraco.cms.presentation.Trees; + +namespace Umbraco.Tests.TreesAndSections +{ + [TestFixture] + public class BaseContentTreeTests + { + + [TearDown] + public void TestTearDown() + { + BaseTree.AfterTreeRender -= EventHandler; + BaseTree.BeforeTreeRender -= EventHandler; + } + + [Test] + public void Run_Optimized() + { + var tree1 = new MyOptimizedContentTree1("content"); + var tree2 = new MyOptimizedContentTree2("content"); + + Assert.IsTrue(tree1.UseOptimizedRendering); + Assert.IsTrue(tree2.UseOptimizedRendering); + } + + [Test] + public void Not_Optimized_Events_AfterRender() + { + var tree = new MyOptimizedContentTree1("content"); + + BaseTree.AfterTreeRender += EventHandler; + + Assert.IsFalse(tree.UseOptimizedRendering); + } + + [Test] + public void Not_Optimized_Events_BeforeRender() + { + var tree = new MyOptimizedContentTree1("content"); + + BaseTree.BeforeTreeRender += EventHandler; + + Assert.IsFalse(tree.UseOptimizedRendering); + } + + [Test] + public void Not_Optimized_Overriden_Method() + { + var tree = new MyNotOptimizedContentTree("content"); + + Assert.IsFalse(tree.UseOptimizedRendering); + } + + private void EventHandler(object sender, TreeEventArgs treeEventArgs) + { + + } + + //optimized because we are not overriding OnRenderNode + public class MyOptimizedContentTree1 : BaseContentTree + { + public MyOptimizedContentTree1(string application) + : base(application) + { + } + + protected override void CreateRootNode(ref XmlTreeNode rootNode) + { + + } + } + + public class MyOptimizedContentTree2 : BaseContentTree + { + public MyOptimizedContentTree2(string application) + : base(application) + { + } + + protected override bool LoadMinimalDocument + { + get { return true; } + } + + protected override void CreateRootNode(ref XmlTreeNode rootNode) + { + + } + + //even if we override it will still be optimized because of the LoadMinimalDocument flag + protected override void OnRenderNode(ref XmlTreeNode xNode, umbraco.cms.businesslogic.web.Document doc) + { + base.OnRenderNode(ref xNode, doc); + } + } + + public class MyNotOptimizedContentTree : BaseContentTree + { + public MyNotOptimizedContentTree(string application) + : base(application) + { + } + + protected override void CreateRootNode(ref XmlTreeNode rootNode) + { + + } + + protected override bool LoadMinimalDocument + { + get { return false; } + } + + protected override void OnRenderNode(ref XmlTreeNode xNode, umbraco.cms.businesslogic.web.Document doc) + { + base.OnRenderNode(ref xNode, doc); + } + } + + + } +} diff --git a/src/Umbraco.Tests/Trees/BaseMediaTreeTests.cs b/src/Umbraco.Tests/TreesAndSections/BaseMediaTreeTests.cs similarity index 93% rename from src/Umbraco.Tests/Trees/BaseMediaTreeTests.cs rename to src/Umbraco.Tests/TreesAndSections/BaseMediaTreeTests.cs index 5aa0840edf..49049d10ec 100644 --- a/src/Umbraco.Tests/Trees/BaseMediaTreeTests.cs +++ b/src/Umbraco.Tests/TreesAndSections/BaseMediaTreeTests.cs @@ -1,65 +1,65 @@ -using NUnit.Framework; -using umbraco.cms.presentation.Trees; - -namespace Umbraco.Tests.Trees -{ - [TestFixture] - public class BaseMediaTreeTests - { - - [TearDown] - public void TestTearDown() - { - BaseTree.AfterTreeRender -= EventHandler; - BaseTree.BeforeTreeRender -= EventHandler; - } - - [Test] - public void Run_Optimized() - { - var tree = new MyOptimizedMediaTree("media"); - - Assert.IsTrue(tree.UseOptimizedRendering); - } - - [Test] - public void Not_Optimized_Events_AfterRender() - { - var tree = new MyOptimizedMediaTree("media"); - - BaseTree.AfterTreeRender += EventHandler; - - Assert.IsFalse(tree.UseOptimizedRendering); - } - - [Test] - public void Not_Optimized_Events_BeforeRender() - { - var tree = new MyOptimizedMediaTree("media"); - - BaseTree.BeforeTreeRender += EventHandler; - - Assert.IsFalse(tree.UseOptimizedRendering); - } - - private void EventHandler(object sender, TreeEventArgs treeEventArgs) - { - - } - - public class MyOptimizedMediaTree : BaseMediaTree - { - public MyOptimizedMediaTree(string application) - : base(application) - { - } - - protected override void CreateRootNode(ref XmlTreeNode rootNode) - { - - } - } - - - } +using NUnit.Framework; +using umbraco.cms.presentation.Trees; + +namespace Umbraco.Tests.TreesAndSections +{ + [TestFixture] + public class BaseMediaTreeTests + { + + [TearDown] + public void TestTearDown() + { + BaseTree.AfterTreeRender -= EventHandler; + BaseTree.BeforeTreeRender -= EventHandler; + } + + [Test] + public void Run_Optimized() + { + var tree = new MyOptimizedMediaTree("media"); + + Assert.IsTrue(tree.UseOptimizedRendering); + } + + [Test] + public void Not_Optimized_Events_AfterRender() + { + var tree = new MyOptimizedMediaTree("media"); + + BaseTree.AfterTreeRender += EventHandler; + + Assert.IsFalse(tree.UseOptimizedRendering); + } + + [Test] + public void Not_Optimized_Events_BeforeRender() + { + var tree = new MyOptimizedMediaTree("media"); + + BaseTree.BeforeTreeRender += EventHandler; + + Assert.IsFalse(tree.UseOptimizedRendering); + } + + private void EventHandler(object sender, TreeEventArgs treeEventArgs) + { + + } + + public class MyOptimizedMediaTree : BaseMediaTree + { + public MyOptimizedMediaTree(string application) + : base(application) + { + } + + protected override void CreateRootNode(ref XmlTreeNode rootNode) + { + + } + } + + + } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 577f43dae8..8bb0da4a0d 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -282,7 +282,7 @@ - + @@ -318,6 +318,8 @@ + + @@ -382,7 +384,7 @@ - + @@ -470,15 +472,15 @@ ImportResources.resx - - + + - + @@ -496,11 +498,11 @@ - - + + - + @@ -520,11 +522,11 @@ - + - - + + @@ -534,7 +536,7 @@ - + @@ -704,9 +706,7 @@ - - - + diff --git a/src/Umbraco.Web.UI/config/log4net.Release.config b/src/Umbraco.Web.UI/config/log4net.Release.config index 384124e719..13297c8a49 100644 --- a/src/Umbraco.Web.UI/config/log4net.Release.config +++ b/src/Umbraco.Web.UI/config/log4net.Release.config @@ -1,17 +1,11 @@ + - - - - - + @@ -23,4 +17,12 @@ + + + + + + + + diff --git a/src/Umbraco.Web.UI/config/log4net.config b/src/Umbraco.Web.UI/config/log4net.config index 10808bb091..0a0e9e4ff7 100644 --- a/src/Umbraco.Web.UI/config/log4net.config +++ b/src/Umbraco.Web.UI/config/log4net.config @@ -1,17 +1,12 @@ + - - - - + @@ -23,4 +18,12 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs new file mode 100644 index 0000000000..a0c6720a88 --- /dev/null +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -0,0 +1,381 @@ +using System; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Hosting; +using Umbraco.Core.Logging; + +namespace Umbraco.Web.Scheduling +{ + /// + /// This is used to create a background task runner which will stay alive in the background of and complete + /// any tasks that are queued. It is web aware and will ensure that it is shutdown correctly when the app domain + /// is shutdown. + /// + /// + internal class BackgroundTaskRunner : IDisposable, IRegisteredObject + where T : IBackgroundTask + { + private readonly bool _dedicatedThread; + private readonly bool _persistentThread; + private readonly BlockingCollection _tasks = new BlockingCollection(); + private Task _consumer; + + private volatile bool _isRunning = false; + private static readonly object Locker = new object(); + private CancellationTokenSource _tokenSource; + internal event EventHandler> TaskError; + internal event EventHandler> TaskStarting; + internal event EventHandler> TaskCompleted; + internal event EventHandler> TaskCancelled; + + public BackgroundTaskRunner(bool dedicatedThread = false, bool persistentThread = false) + { + _dedicatedThread = dedicatedThread; + _persistentThread = persistentThread; + HostingEnvironment.RegisterObject(this); + } + + public int TaskCount + { + get { return _tasks.Count; } + } + + public bool IsRunning + { + get { return _isRunning; } + } + + public TaskStatus TaskStatus + { + get { return _consumer.Status; } + } + + + /// + /// Returns the task awaiter so that consumers of the BackgroundTaskManager can await + /// the threading operation. + /// + /// + /// + /// This is just the coolest thing ever, check this article out: + /// http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115642.aspx + /// + /// So long as we have a method called GetAwaiter() that returns an instance of INotifyCompletion + /// we can await anything! :) + /// + public TaskAwaiter GetAwaiter() + { + return _consumer.GetAwaiter(); + } + + public void Add(T task) + { + //add any tasks first + LogHelper.Debug>(" Task added {0}", () => task.GetType()); + _tasks.Add(task); + + //ensure's everything is started + StartUp(); + } + + public void StartUp() + { + if (!_isRunning) + { + lock (Locker) + { + //double check + if (!_isRunning) + { + _isRunning = true; + //Create a new token source since this is a new proces + _tokenSource = new CancellationTokenSource(); + StartConsumer(); + LogHelper.Debug>("Starting"); + } + } + } + } + + public void ShutDown() + { + lock (Locker) + { + _isRunning = false; + + try + { + if (_consumer != null) + { + //cancel all operations + _tokenSource.Cancel(); + + try + { + _consumer.Wait(); + } + catch (AggregateException e) + { + //NOTE: We are logging Debug because we are expecting these errors + + LogHelper.Debug>("AggregateException thrown with the following inner exceptions:"); + // Display information about each exception. + foreach (var v in e.InnerExceptions) + { + var exception = v as TaskCanceledException; + if (exception != null) + { + LogHelper.Debug>(" .Net TaskCanceledException: .Net Task ID {0}", () => exception.Task.Id); + } + else + { + LogHelper.Debug>(" Exception: {0}", () => v.GetType().Name); + } + } + } + } + + if (_tasks.Count > 0) + { + LogHelper.Debug>("Processing remaining tasks before shutdown: {0}", () => _tasks.Count); + + //now we need to ensure the remaining queue is processed if there's any remaining, + // this will all be processed on the current/main thread. + T remainingTask; + while (_tasks.TryTake(out remainingTask)) + { + ConsumeTaskInternal(remainingTask); + } + } + + LogHelper.Debug>("Shutdown"); + + //disposing these is really optional since they'll be disposed immediately since they are no longer running + //but we'll put this here anyways. + if (_consumer != null && (_consumer.IsCompleted || _consumer.IsCanceled)) + { + _consumer.Dispose(); + } + } + catch (Exception ex) + { + LogHelper.Error>("Error occurred shutting down task runner", ex); + } + finally + { + HostingEnvironment.UnregisterObject(this); + } + } + } + + /// + /// Starts the consumer task + /// + private void StartConsumer() + { + var token = _tokenSource.Token; + + _consumer = Task.Factory.StartNew(() => + StartThread(token), + token, + _dedicatedThread ? TaskCreationOptions.LongRunning : TaskCreationOptions.None, + TaskScheduler.Default); + + //if this is not a persistent thread, wait till it's done and shut ourselves down + // thus ending the thread or giving back to the thread pool. If another task is added + // another thread will spawn or be taken from the pool to process. + if (!_persistentThread) + { + _consumer.ContinueWith(task => ShutDown()); + } + + } + + /// + /// Invokes a new worker thread to consume tasks + /// + /// + private void StartThread(CancellationToken token) + { + // Was cancellation already requested? + if (token.IsCancellationRequested) + { + LogHelper.Info>("Thread {0} was cancelled before it got started.", () => Thread.CurrentThread.ManagedThreadId); + token.ThrowIfCancellationRequested(); + } + + TakeAndConsumeTask(token); + } + + /// + /// Trys to get a task from the queue, if there isn't one it will wait a second and try again + /// + /// + private void TakeAndConsumeTask(CancellationToken token) + { + if (token.IsCancellationRequested) + { + LogHelper.Info>("Thread {0} was cancelled.", () => Thread.CurrentThread.ManagedThreadId); + token.ThrowIfCancellationRequested(); + } + + //If this is true, the thread will stay alive and just wait until there is anything in the queue + // and process it. When there is nothing in the queue, the thread will just block until there is + // something to process. + //When this is false, the thread will process what is currently in the queue and once that is + // done, the thread will end and we will shutdown the process + + if (_persistentThread) + { + //This will iterate over the collection, if there is nothing to take + // the thread will block until there is something available. + //We need to pass our cancellation token so that the thread will + // cancel when we shutdown + foreach (var t in _tasks.GetConsumingEnumerable(token)) + { + ConsumeTaskCancellable(t, token); + } + + //recurse and keep going + TakeAndConsumeTask(token); + } + else + { + T repositoryTask; + while (_tasks.TryTake(out repositoryTask)) + { + ConsumeTaskCancellable(repositoryTask, token); + } + + //the task will end here + } + } + + internal void ConsumeTaskCancellable(T task, CancellationToken token) + { + if (token.IsCancellationRequested) + { + OnTaskCancelled(new TaskEventArgs(task)); + + //NOTE: Since the task hasn't started this is pretty pointless so leaving it out. + LogHelper.Info>("Task {0}) was cancelled.", + () => task.GetType()); + + token.ThrowIfCancellationRequested(); + } + + ConsumeTaskInternal(task); + } + + private void ConsumeTaskInternal(T task) + { + try + { + OnTaskStarting(new TaskEventArgs(task)); + + try + { + using (task) + { + task.Run(); + } + } + catch (Exception e) + { + OnTaskError(new TaskEventArgs(task, e)); + throw; + } + + OnTaskCompleted(new TaskEventArgs(task)); + } + catch (Exception ex) + { + LogHelper.Error>("An error occurred consuming task", ex); + } + } + + protected virtual void OnTaskError(TaskEventArgs e) + { + var handler = TaskError; + if (handler != null) handler(this, e); + } + + protected virtual void OnTaskStarting(TaskEventArgs e) + { + var handler = TaskStarting; + if (handler != null) handler(this, e); + } + + protected virtual void OnTaskCompleted(TaskEventArgs e) + { + var handler = TaskCompleted; + if (handler != null) handler(this, e); + } + + protected virtual void OnTaskCancelled(TaskEventArgs e) + { + var handler = TaskCancelled; + if (handler != null) handler(this, e); + } + + + #region Disposal + private readonly object _disposalLocker = new object(); + public bool IsDisposed { get; private set; } + + ~BackgroundTaskRunner() + { + this.Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (this.IsDisposed || !disposing) + return; + lock (_disposalLocker) + { + if (IsDisposed) + return; + DisposeResources(); + IsDisposed = true; + } + } + + protected virtual void DisposeResources() + { + ShutDown(); + } + #endregion + + public void Stop(bool immediate) + { + if (immediate == false) + { + LogHelper.Debug>("Application is shutting down, waiting for tasks to complete"); + Dispose(); + } + else + { + //NOTE: this will thread block the current operation if the manager + // is still shutting down because the Shutdown operation is also locked + // by this same lock instance. This would only matter if Stop is called by ASP.Net + // on two different threads though, otherwise the current thread will just block normally + // until the app is shutdown + lock (Locker) + { + LogHelper.Info>("Application is shutting down immediately"); + } + } + + } + + } +} diff --git a/src/Umbraco.Web/Scheduling/IBackgroundTask.cs b/src/Umbraco.Web/Scheduling/IBackgroundTask.cs new file mode 100644 index 0000000000..3e0bddfddb --- /dev/null +++ b/src/Umbraco.Web/Scheduling/IBackgroundTask.cs @@ -0,0 +1,9 @@ +using System; + +namespace Umbraco.Web.Scheduling +{ + internal interface IBackgroundTask : IDisposable + { + void Run(); + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs index 058957001d..67a2f4a448 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -1,6 +1,8 @@ using System; using System.Net; using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.Sync; @@ -8,11 +10,13 @@ namespace Umbraco.Web.Scheduling { internal class KeepAlive { - public static void Start(object sender) + public static void Start(ApplicationContext appContext, IUmbracoSettingsSection settings) { using (DisposableTimer.DebugDuration(() => "Keep alive executing", () => "Keep alive complete")) { - var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl(); + var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl( + appContext, + settings); if (string.IsNullOrWhiteSpace(umbracoBaseUrl)) { @@ -20,7 +24,7 @@ namespace Umbraco.Web.Scheduling } else { - var url = string.Format("{0}/ping.aspx", umbracoBaseUrl); + var url = string.Format("{0}ping.aspx", umbracoBaseUrl.EnsureEndsWith('/')); try { diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index cea4d1b01c..933e6d7c6b 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -2,47 +2,31 @@ using System; using System.Web; using System.Web.Caching; using umbraco.BusinessLogic; +using Umbraco.Core; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; namespace Umbraco.Web.Scheduling { - //TODO: Refactor this to use a normal scheduling processor! - internal class LogScrubber + internal class LogScrubber : DisposableObject, IBackgroundTask { - // this is a raw copy of the legacy code in all its uglyness + private readonly ApplicationContext _appContext; + private readonly IUmbracoSettingsSection _settings; - CacheItemRemovedCallback _onCacheRemove; - const string LogScrubberTaskName = "ScrubLogs"; - - public void Start() + public LogScrubber(ApplicationContext appContext, IUmbracoSettingsSection settings) { - // log scrubbing - AddTask(LogScrubberTaskName, GetLogScrubbingInterval()); + _appContext = appContext; + _settings = settings; } - private static int GetLogScrubbingInterval() - { - int interval = 24 * 60 * 60; //24 hours - try - { - if (global::umbraco.UmbracoSettings.CleaningMiliseconds > -1) - interval = global::umbraco.UmbracoSettings.CleaningMiliseconds; - } - catch (Exception e) - { - LogHelper.Error("Unable to locate a log scrubbing interval. Defaulting to 24 horus", e); - } - return interval; - } - - private static int GetLogScrubbingMaximumAge() + private int GetLogScrubbingMaximumAge(IUmbracoSettingsSection settings) { int maximumAge = 24 * 60 * 60; try { - if (global::umbraco.UmbracoSettings.MaxLogAge > -1) - maximumAge = global::umbraco.UmbracoSettings.MaxLogAge; + if (settings.Logging.MaxLogAge > -1) + maximumAge = settings.Logging.MaxLogAge; } catch (Exception e) { @@ -52,26 +36,19 @@ namespace Umbraco.Web.Scheduling } - private void AddTask(string name, int seconds) - { - _onCacheRemove = new CacheItemRemovedCallback(CacheItemRemoved); - HttpRuntime.Cache.Insert(name, seconds, null, - DateTime.Now.AddSeconds(seconds), System.Web.Caching.Cache.NoSlidingExpiration, - CacheItemPriority.NotRemovable, _onCacheRemove); + /// + /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. + /// + protected override void DisposeResources() + { } - public void CacheItemRemoved(string k, object v, CacheItemRemovedReason r) + public void Run() { - if (k.Equals(LogScrubberTaskName)) + using (DisposableTimer.DebugDuration(() => "Log scrubbing executing", () => "Log scrubbing complete")) { - ScrubLogs(); + Log.CleanLogs(GetLogScrubbingMaximumAge(_settings)); } - AddTask(k, Convert.ToInt32(v)); - } - - private static void ScrubLogs() - { - Log.CleanLogs(GetLogScrubbingMaximumAge()); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index d8649134e8..b8d6d0bc91 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Net; using System.Text; using Umbraco.Core; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.Publishing; using Umbraco.Core.Sync; @@ -10,23 +11,40 @@ using Umbraco.Web.Mvc; namespace Umbraco.Web.Scheduling { - internal class ScheduledPublishing + internal class ScheduledPublishing : DisposableObject, IBackgroundTask { + private readonly ApplicationContext _appContext; + private readonly IUmbracoSettingsSection _settings; + private static bool _isPublishingRunning = false; - public void Start(ApplicationContext appContext) + public ScheduledPublishing(ApplicationContext appContext, IUmbracoSettingsSection settings) { - if (appContext == null) return; + _appContext = appContext; + _settings = settings; + } + + + /// + /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. + /// + protected override void DisposeResources() + { + } + + public void Run() + { + if (_appContext == null) return; using (DisposableTimer.DebugDuration(() => "Scheduled publishing executing", () => "Scheduled publishing complete")) - { + { if (_isPublishingRunning) return; _isPublishingRunning = true; - + try { - var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl(); + var umbracoBaseUrl = ServerEnvironmentHelper.GetCurrentServerUmbracoBaseUrl(_appContext, _settings); if (string.IsNullOrWhiteSpace(umbracoBaseUrl)) { @@ -34,14 +52,15 @@ namespace Umbraco.Web.Scheduling } else { - var url = string.Format("{0}/RestServices/ScheduledPublish/Index", umbracoBaseUrl); + var url = string.Format("{0}RestServices/ScheduledPublish/Index", umbracoBaseUrl.EnsureEndsWith('/')); using (var wc = new WebClient()) { //pass custom the authorization header - wc.Headers.Set("Authorization", AdminTokenAuthorizeAttribute.GetAuthHeaderTokenVal(appContext)); + wc.Headers.Set("Authorization", AdminTokenAuthorizeAttribute.GetAuthHeaderTokenVal(_appContext)); var result = wc.UploadString(url, ""); } + } } } catch (Exception ee) @@ -54,7 +73,5 @@ namespace Umbraco.Web.Scheduling } } } - - } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index e4ce265444..cfebfbb246 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; using System.Xml; using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.Publishing; using Umbraco.Core.Sync; @@ -15,40 +16,25 @@ namespace Umbraco.Web.Scheduling // would need to be a publicly available task (URL) which isn't really very good :( // We should really be using the AdminTokenAuthorizeAttribute for this stuff - internal class ScheduledTasks + internal class ScheduledTasks : DisposableObject, IBackgroundTask { + private readonly ApplicationContext _appContext; + private readonly IUmbracoSettingsSection _settings; private static readonly Hashtable ScheduledTaskTimes = new Hashtable(); + private static bool _isPublishingRunning = false; - public void Start(ApplicationContext appContext) + public ScheduledTasks(ApplicationContext appContext, IUmbracoSettingsSection settings) { - using (DisposableTimer.DebugDuration(() => "Scheduled tasks executing", () => "Scheduled tasks complete")) - { - if (_isPublishingRunning) return; - - _isPublishingRunning = true; - - try - { - ProcessTasks(); - } - catch (Exception ee) - { - LogHelper.Error("Error executing scheduled task", ee); - } - finally - { - _isPublishingRunning = false; - } - } - + _appContext = appContext; + _settings = settings; } - private static void ProcessTasks() + private void ProcessTasks() { - var scheduledTasks = UmbracoConfig.For.UmbracoSettings().ScheduledTasks.Tasks; + var scheduledTasks = _settings.ScheduledTasks.Tasks; foreach (var t in scheduledTasks) { var runTask = false; @@ -75,7 +61,7 @@ namespace Umbraco.Web.Scheduling } } - private static bool GetTaskByHttp(string url) + private bool GetTaskByHttp(string url) { var myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url); @@ -93,5 +79,35 @@ namespace Umbraco.Web.Scheduling return false; } + + /// + /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. + /// + protected override void DisposeResources() + { + } + + public void Run() + { + using (DisposableTimer.DebugDuration(() => "Scheduled tasks executing", () => "Scheduled tasks complete")) + { + if (_isPublishingRunning) return; + + _isPublishingRunning = true; + + try + { + ProcessTasks(); + } + catch (Exception ee) + { + LogHelper.Error("Error executing scheduled task", ee); + } + finally + { + _isPublishingRunning = false; + } + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/Scheduler.cs b/src/Umbraco.Web/Scheduling/Scheduler.cs index e18e41aeda..5944c301d4 100644 --- a/src/Umbraco.Web/Scheduling/Scheduler.cs +++ b/src/Umbraco.Web/Scheduling/Scheduler.cs @@ -1,5 +1,9 @@ -using System.Threading; +using System; +using System.Threading; +using System.Web; using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.Sync; @@ -9,65 +13,107 @@ namespace Umbraco.Web.Scheduling /// Used to do the scheduling for tasks, publishing, etc... ///
/// - /// - /// TODO: Much of this code is legacy and needs to be updated, there are a few new/better ways to do scheduling - /// in a web project nowadays. - /// - /// //TODO: We need a much more robust way of handing scheduled tasks and also need to take into account app shutdowns during - /// a scheduled tasks operation - /// http://haacked.com/archive/2011/10/16/the-dangers-of-implementing-recurring-background-tasks-in-asp-net.aspx/ - /// + /// All tasks are run in a background task runner which is web aware and will wind down the task correctly instead of killing it completely when + /// the app domain shuts down. /// internal sealed class Scheduler : ApplicationEventHandler { private static Timer _pingTimer; private static Timer _schedulingTimer; - private static LogScrubber _scrubber; + private static BackgroundTaskRunner _publishingRunner; + private static BackgroundTaskRunner _tasksRunner; + private static BackgroundTaskRunner _scrubberRunner; + private static Timer _logScrubberTimer; + private static volatile bool _started = false; + private static readonly object Locker = new object(); protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { if (umbracoApplication.Context == null) return; - LogHelper.Debug(() => "Initializing the scheduler"); + //subscribe to app init so we can subsribe to the application events + UmbracoApplicationBase.ApplicationInit += (sender, args) => + { + var app = (HttpApplication)sender; - // time to setup the tasks + //subscribe to the end of a successful request (a handler actually executed) + app.PostRequestHandlerExecute += (o, eventArgs) => + { + if (_started == false) + { + lock (Locker) + { + if (_started == false) + { + _started = true; + LogHelper.Debug(() => "Initializing the scheduler"); - // these are the legacy tasks - // just copied over here for backward compatibility - // of course we should have a proper scheduler, see #U4-809 + // time to setup the tasks - //NOTE: It is important to note that we need to use the ctor for a timer without the 'state' object specified, this is in order - // to ensure that the timer itself is not GC'd since internally .net will pass itself in as the state object and that will keep it alive. - // There's references to this here: http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer - // we also make these timers static to ensure further GC safety. + //We have 3 background runners that are web aware, if the app domain dies, these tasks will wind down correctly + _publishingRunner = new BackgroundTaskRunner(); + _tasksRunner = new BackgroundTaskRunner(); + _scrubberRunner = new BackgroundTaskRunner(); - // ping/keepalive - _pingTimer = new Timer(KeepAlive.Start); - _pingTimer.Change(60000, 300000); + //NOTE: It is important to note that we need to use the ctor for a timer without the 'state' object specified, this is in order + // to ensure that the timer itself is not GC'd since internally .net will pass itself in as the state object and that will keep it alive. + // There's references to this here: http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer + // we also make these timers static to ensure further GC safety. - // scheduled publishing/unpublishing - _schedulingTimer = new Timer(PerformScheduling); - _schedulingTimer.Change(30000, 60000); + // ping/keepalive - NOTE: we don't use a background runner for this because it does not need to be web aware, if the app domain dies, no problem + _pingTimer = new Timer(state => KeepAlive.Start(applicationContext, UmbracoConfig.For.UmbracoSettings())); + _pingTimer.Change(60000, 300000); - //log scrubbing - _scrubber = new LogScrubber(); - _scrubber.Start(); + // scheduled publishing/unpublishing + _schedulingTimer = new Timer(state => PerformScheduling(applicationContext, UmbracoConfig.For.UmbracoSettings())); + _schedulingTimer.Change(60000, 60000); + + //log scrubbing + _logScrubberTimer = new Timer(state => PerformLogScrub(applicationContext, UmbracoConfig.For.UmbracoSettings())); + _logScrubberTimer.Change(60000, GetLogScrubbingInterval(UmbracoConfig.For.UmbracoSettings())); + } + } + } + }; + }; + } + + + private int GetLogScrubbingInterval(IUmbracoSettingsSection settings) + { + int interval = 24 * 60 * 60; //24 hours + try + { + if (settings.Logging.CleaningMiliseconds > -1) + interval = settings.Logging.CleaningMiliseconds; + } + catch (Exception e) + { + LogHelper.Error("Unable to locate a log scrubbing interval. Defaulting to 24 horus", e); + } + return interval; + } + + private static void PerformLogScrub(ApplicationContext appContext, IUmbracoSettingsSection settings) + { + _scrubberRunner.Add(new LogScrubber(appContext, settings)); } /// /// This performs all of the scheduling on the one timer /// - /// + /// + /// /// /// No processing will be done if this server is a slave /// - private static void PerformScheduling(object sender) + private static void PerformScheduling(ApplicationContext appContext, IUmbracoSettingsSection settings) { using (DisposableTimer.DebugDuration(() => "Scheduling interval executing", () => "Scheduling interval complete")) { //get the current server status to see if this server should execute the scheduled publishing - var serverStatus = ServerEnvironmentHelper.GetStatus(); + var serverStatus = ServerEnvironmentHelper.GetStatus(settings); switch (serverStatus) { @@ -78,13 +124,11 @@ namespace Umbraco.Web.Scheduling // then we will process the scheduling //do the scheduled publishing - var scheduledPublishing = new ScheduledPublishing(); - scheduledPublishing.Start(ApplicationContext.Current); - + _publishingRunner.Add(new ScheduledPublishing(appContext, settings)); + //do the scheduled tasks - var scheduledTasks = new ScheduledTasks(); - scheduledTasks.Start(ApplicationContext.Current); - + _tasksRunner.Add(new ScheduledTasks(appContext, settings)); + break; case CurrentServerEnvironmentStatus.Slave: //do not process @@ -97,5 +141,6 @@ namespace Umbraco.Web.Scheduling } } + } } diff --git a/src/Umbraco.Web/Scheduling/TaskEventArgs.cs b/src/Umbraco.Web/Scheduling/TaskEventArgs.cs new file mode 100644 index 0000000000..2ce64a239f --- /dev/null +++ b/src/Umbraco.Web/Scheduling/TaskEventArgs.cs @@ -0,0 +1,22 @@ +using System; + +namespace Umbraco.Web.Scheduling +{ + internal class TaskEventArgs : EventArgs + where T : IBackgroundTask + { + public T Task { get; private set; } + public Exception Exception { get; private set; } + + public TaskEventArgs(T task) + { + Task = task; + } + + public TaskEventArgs(T task, Exception exception) + { + Task = task; + Exception = exception; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Trees/ApplicationTreeRegistrar.cs b/src/Umbraco.Web/Trees/ApplicationTreeRegistrar.cs index 1ed7e0a6b1..1b125bce63 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeRegistrar.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeRegistrar.cs @@ -1,68 +1,99 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Umbraco.Core; using Umbraco.Core.Models; using umbraco.businesslogic; +using Umbraco.Core.Services; namespace Umbraco.Web.Trees { - //TODO: Is there any way to get this to execute lazily when needed? - // i.e. When the back office loads so that this doesn't execute on startup for a content request. - /// /// A startup handler for putting the tree 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 sealed class ApplicationTreeRegistrar : ApplicationEventHandler { protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { - ScanTrees(applicationContext); + //Call initialize on the tree service with the lazy enumerable class below, when the tree service needs to resolve, + // it will lazily do the scanning and comparing so it's not actually done on app start. + applicationContext.Services.ApplicationTreeService.Intitialize(new LazyEnumerableTrees()); } /// - /// Scans for all attributed trees and ensures they exist in the tree xml + /// This class is here so that we can provide lazy access to tree scanning for when it is needed /// - private static void ScanTrees(ApplicationContext applicationContext) + private class LazyEnumerableTrees : IEnumerable { - var added = new List(); + public LazyEnumerableTrees() + { + _lazyTrees = new Lazy>(() => + { + var added = new List(); - // Load all Controller Trees by attribute and add them to the XML config - // we also need to make sure that any alias added with the new trees is not also added - // with the legacy trees. - var types = PluginManager.Current.ResolveAttributedTreeControllers(); + // Load all Controller Trees by attribute + var types = PluginManager.Current.ResolveAttributedTreeControllers(); + //convert them to ApplicationTree instances + var items = types + .Select(x => + new Tuple(x, x.GetCustomAttributes(false).Single())) + .Select(x => new ApplicationTree(x.Item2.Initialize, x.Item2.SortOrder, x.Item2.ApplicationAlias, x.Item2.Alias, x.Item2.Title, x.Item2.IconClosed, x.Item2.IconOpen, x.Item1.GetFullNameWithAssembly())) + .ToArray(); - //get all non-legacy application tree's - var items = types - .Select(x => - new Tuple(x, x.GetCustomAttributes(false).Single())) - .Where(x => applicationContext.Services.ApplicationTreeService.GetByAlias(x.Item2.Alias) == null) - .Select(x => new ApplicationTree(x.Item2.Initialize, x.Item2.SortOrder, x.Item2.ApplicationAlias, x.Item2.Alias, x.Item2.Title, x.Item2.IconClosed, x.Item2.IconOpen, x.Item1.GetFullNameWithAssembly())) - .ToArray(); - - added.AddRange(items.Select(x => x.Alias)); + added.AddRange(items.Select(x => x.Alias)); - //find the legacy trees - var legacyTreeTypes = PluginManager.Current.ResolveAttributedTrees(); - - var legacyItems = legacyTreeTypes - .Select(x => + //find the legacy trees + var legacyTreeTypes = PluginManager.Current.ResolveAttributedTrees(); + //convert them to ApplicationTree instances + var legacyItems = legacyTreeTypes + .Select(x => new Tuple( - x, + new Tuple( + x, x.GetCustomAttributes(false).SingleOrDefault(), + x.GetCustomAttributes(false).SingleOrDefault())) x.GetCustomAttributes(false).SingleOrDefault())) //ensure that the legacy tree attribute exists - .Where(x => x.Item2 != null) + .Where(x => x.Item2 != null) //ensure that it's not obsoleted, any obsoleted tree will not be auto added to the config .Where(x => x.Item3 == null) - .Where(x => applicationContext.Services.ApplicationTreeService.GetByAlias(x.Item2.Alias) == null - //make sure the legacy tree isn't added on top of the controller tree! - && added.InvariantContains(x.Item2.Alias) == false) - .Select(x => new ApplicationTree(x.Item2.Initialize, x.Item2.SortOrder, x.Item2.ApplicationAlias, x.Item2.Alias, x.Item2.Title, x.Item2.IconClosed, x.Item2.IconOpen, x.Item1.GetFullNameWithAssembly())); + //make sure the legacy tree isn't added on top of the controller tree! + .Where(x => added.InvariantContains(x.Item2.Alias) == false) + .Select(x => new ApplicationTree(x.Item2.Initialize, x.Item2.SortOrder, x.Item2.ApplicationAlias, x.Item2.Alias, x.Item2.Title, x.Item2.IconClosed, x.Item2.IconOpen, x.Item1.GetFullNameWithAssembly())); - applicationContext.Services.ApplicationTreeService.Intitialize(items.Concat(legacyItems)); + return items.Concat(legacyItems).ToArray(); + }); + } + private readonly Lazy> _lazyTrees; + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() + { + return _lazyTrees.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(); + } } } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 2b2ef055af..315b54da29 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -268,6 +268,7 @@ + @@ -494,6 +495,8 @@ + + diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/TreeDefinitionCollection.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/TreeDefinitionCollection.cs index 16e034b8f5..498af0da4a 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/TreeDefinitionCollection.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/TreeDefinitionCollection.cs @@ -28,8 +28,9 @@ namespace umbraco.cms.presentation.Trees //create singleton private static readonly TreeDefinitionCollection instance = new TreeDefinitionCollection(); - private static readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(); - + private static readonly object Locker = new object(); + private static volatile bool _ensureTrees = false; + public static TreeDefinitionCollection Instance { get @@ -128,7 +129,12 @@ namespace umbraco.cms.presentation.Trees public void ReRegisterTrees() { - EnsureTreesRegistered(true); + //clears the trees/flag so that they are lazily refreshed on next access + lock (Locker) + { + this.Clear(); + _ensureTrees = false; + } } /// @@ -137,70 +143,68 @@ namespace umbraco.cms.presentation.Trees /// This will also store an instance of each tree object in the TreeDefinition class which should be /// used when referencing all tree classes. /// - private void EnsureTreesRegistered(bool clearFirst = false) + private void EnsureTreesRegistered() { - using (var l = new UpgradeableReadLock(Lock)) - { - if (clearFirst) - { - this.Clear(); - } + if (_ensureTrees == false) + { + lock (Locker) + { + if (_ensureTrees == false) + { - //if we already have tree, exit - if (this.Count > 0) - return; + var foundITrees = PluginManager.Current.ResolveTrees(); - l.UpgradeToWriteLock(); + var objTrees = ApplicationTree.getAll(); + var appTrees = new List(); + appTrees.AddRange(objTrees); + + var apps = Application.getAll(); + + foreach (var type in foundITrees) + { + + //find the Application tree's who's combination of assembly name and tree type is equal to + //the Type that was found's full name. + //Since a tree can exist in multiple applications we'll need to register them all. + + //The logic of this has changed in 6.0: http://issues.umbraco.org/issue/U4-1360 + // we will support the old legacy way but the normal way is to match on assembly qualified names + + var appTreesForType = appTrees.FindAll( + tree => + { + //match the type on assembly qualified name if the assembly attribute is empty or if the + // tree type contains a comma (meaning it is assembly qualified) + if (tree.AssemblyName.IsNullOrWhiteSpace() || tree.Type.Contains(",")) + { + return tree.GetRuntimeType() == type; + } + + //otherwise match using legacy match rules + return (string.Format("{0}.{1}", tree.AssemblyName, tree.Type).InvariantEquals(type.FullName)); + } + ); + + foreach (var appTree in appTreesForType) + { + //find the Application object whos name is the same as our appTree ApplicationAlias + var app = apps.Find( + a => (a.alias == appTree.ApplicationAlias) + ); + + var def = new TreeDefinition(type, appTree, app); + this.Add(def); + } + } + //sort our trees with the sort order definition + this.Sort((t1, t2) => t1.Tree.SortOrder.CompareTo(t2.Tree.SortOrder)); + + _ensureTrees = true; + } + } + } - var foundITrees = PluginManager.Current.ResolveTrees(); - - var objTrees = ApplicationTree.getAll(); - var appTrees = new List(); - appTrees.AddRange(objTrees); - - var apps = Application.getAll(); - - foreach (var type in foundITrees) - { - - //find the Application tree's who's combination of assembly name and tree type is equal to - //the Type that was found's full name. - //Since a tree can exist in multiple applications we'll need to register them all. - - //The logic of this has changed in 6.0: http://issues.umbraco.org/issue/U4-1360 - // we will support the old legacy way but the normal way is to match on assembly qualified names - - var appTreesForType = appTrees.FindAll( - tree => - { - //match the type on assembly qualified name if the assembly attribute is empty or if the - // tree type contains a comma (meaning it is assembly qualified) - if (tree.AssemblyName.IsNullOrWhiteSpace() || tree.Type.Contains(",")) - { - return tree.GetRuntimeType() == type; - } - - //otherwise match using legacy match rules - return (string.Format("{0}.{1}", tree.AssemblyName, tree.Type).InvariantEquals(type.FullName)); - } - ); - - foreach (var appTree in appTreesForType) - { - //find the Application object whos name is the same as our appTree ApplicationAlias - var app = apps.Find( - a => (a.alias == appTree.ApplicationAlias) - ); - - var def = new TreeDefinition(type, appTree, app); - this.Add(def); - } - } - //sort our trees with the sort order definition - this.Sort((t1, t2) => t1.Tree.SortOrder.CompareTo(t2.Tree.SortOrder)); - - } } } diff --git a/src/UmbracoExamine/LocalStorage/LocalTempStorageDirectory.cs b/src/UmbracoExamine/LocalStorage/LocalTempStorageDirectory.cs new file mode 100644 index 0000000000..d945184056 --- /dev/null +++ b/src/UmbracoExamine/LocalStorage/LocalTempStorageDirectory.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Lucene.Net.Store; + +namespace UmbracoExamine.LocalStorage +{ + public class LocalTempStorageDirectory : SimpleFSDirectory + { + private readonly Lucene.Net.Store.Directory _realDirectory; + + public LocalTempStorageDirectory( + DirectoryInfo tempStorageDir, + Lucene.Net.Store.Directory realDirectory) + : base(tempStorageDir) + { + _realDirectory = realDirectory; + Enabled = true; + } + + /// + /// If initialization fails, it will be disabled and then this will just wrap the 'real directory' + /// + internal bool Enabled { get; set; } + + public override string[] ListAll() + { + //always from the real dir + return _realDirectory.ListAll(); + } + + /// Returns true if a file with the given name exists. + public override bool FileExists(string name) + { + //always from the real dir + return _realDirectory.FileExists(name); + } + + /// Returns the time the named file was last modified. + public override long FileModified(string name) + { + //always from the real dir + return _realDirectory.FileModified(name); + } + + /// Set the modified time of an existing file to now. + public override void TouchFile(string name) + { + //always from the real dir + _realDirectory.TouchFile(name); + } + + /// Removes an existing file in the directory. + public override void DeleteFile(string name) + { + //perform on both dirs + if (Enabled) + { + base.DeleteFile(name); + } + + _realDirectory.DeleteFile(name); + } + + /// Returns the length of a file in the directory. + public override long FileLength(string name) + { + //always from the real dir + return _realDirectory.FileLength(name); + } + + /// + /// Creates a new, empty file in the directory with the given name. + /// Returns a stream writing this file. + /// + public override IndexOutput CreateOutput(string name) + { + //write to both indexes + if (Enabled) + { + return new MultiIndexOutput( + base.CreateOutput(name), + _realDirectory.CreateOutput(name)); + } + + return _realDirectory.CreateOutput(name); + } + + /// + /// Returns a stream reading an existing file. + /// + public override IndexInput OpenInput(string name) + { + if (Enabled) + { + //return the reader from the cache, not the real dir + return base.OpenInput(name); + } + + return _realDirectory.OpenInput(name); + } + + public override void Dispose() + { + if (Enabled) + { + base.Dispose(); + } + _realDirectory.Dispose(); + } + + + } +} diff --git a/src/UmbracoExamine/LocalStorage/LocalTempStorageDirectoryTracker.cs b/src/UmbracoExamine/LocalStorage/LocalTempStorageDirectoryTracker.cs new file mode 100644 index 0000000000..d3c94715b3 --- /dev/null +++ b/src/UmbracoExamine/LocalStorage/LocalTempStorageDirectoryTracker.cs @@ -0,0 +1,29 @@ +using System.Collections.Concurrent; +using System.IO; +using Directory = Lucene.Net.Store.Directory; + +namespace UmbracoExamine.LocalStorage +{ + internal class LocalTempStorageDirectoryTracker + { + private readonly static LocalTempStorageDirectoryTracker Instance = new LocalTempStorageDirectoryTracker(); + private readonly ConcurrentDictionary _directories = new ConcurrentDictionary(); + + public static LocalTempStorageDirectoryTracker Current + { + get { return Instance; } + } + + public LocalTempStorageDirectory GetDirectory(DirectoryInfo dir, Directory realDir, bool disable = false) + { + var resolved = _directories.GetOrAdd(dir.FullName, s => new LocalTempStorageDirectory(dir, realDir)); + + if (disable) + { + resolved.Enabled = false; + } + + return resolved; + } + } +} \ No newline at end of file diff --git a/src/UmbracoExamine/LocalStorage/LocalTempStorageIndexer.cs b/src/UmbracoExamine/LocalStorage/LocalTempStorageIndexer.cs new file mode 100644 index 0000000000..2e242c1284 --- /dev/null +++ b/src/UmbracoExamine/LocalStorage/LocalTempStorageIndexer.cs @@ -0,0 +1,132 @@ +using System.Collections.Specialized; +using System.IO; +using System.Linq; +using System.Web; +using Lucene.Net.Analysis; +using Lucene.Net.Index; +using Lucene.Net.Store; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Directory = System.IO.Directory; + +namespace UmbracoExamine.LocalStorage +{ + internal class LocalTempStorageIndexer + { + private string _tempPath; + public Lucene.Net.Store.Directory LuceneDirectory { get; private set; } + private readonly object _locker = new object(); + public SnapshotDeletionPolicy Snapshotter { get; private set; } + + public LocalTempStorageIndexer() + { + IndexDeletionPolicy policy = new KeepOnlyLastCommitDeletionPolicy(); + Snapshotter = new SnapshotDeletionPolicy(policy); + } + + public void Initialize(NameValueCollection config, string configuredPath, Lucene.Net.Store.Directory baseLuceneDirectory, Analyzer analyzer) + { + var codegenPath = HttpRuntime.CodegenDir; + + _tempPath = Path.Combine(codegenPath, configuredPath.TrimStart('~', '/').Replace("/", "\\")); + + var success = InitializeLocalIndexAndDirectory(baseLuceneDirectory, analyzer, configuredPath); + + //create the custom lucene directory which will keep the main and temp FS's in sync + LuceneDirectory = LocalTempStorageDirectoryTracker.Current.GetDirectory( + new DirectoryInfo(_tempPath), + baseLuceneDirectory, + //flag to disable the mirrored folder if not successful + success == false); + } + + private bool InitializeLocalIndexAndDirectory(Lucene.Net.Store.Directory baseLuceneDirectory, Analyzer analyzer, string configuredPath) + { + lock (_locker) + { + if (Directory.Exists(_tempPath) == false) + { + Directory.CreateDirectory(_tempPath); + } + + //copy index + + using (new IndexWriter( + //read from the underlying/default directory, not the temp codegen dir + baseLuceneDirectory, + analyzer, + Snapshotter, + IndexWriter.MaxFieldLength.UNLIMITED)) + { + try + { + var basePath = IOHelper.MapPath(configuredPath); + + var commit = Snapshotter.Snapshot(); + var allSnapshotFiles = commit.GetFileNames().Concat(new[] { commit.GetSegmentsFileName() }) + .Distinct() + .ToArray(); + + var tempDir = new DirectoryInfo(_tempPath); + + //Get all files in the temp storage that don't exist in the snapshot collection, we want to remove these + var toRemove = tempDir.GetFiles() + .Select(x => x.Name) + .Except(allSnapshotFiles); + + using (var tempDirectory = new SimpleFSDirectory(tempDir)) + { + if (IndexWriter.IsLocked(tempDirectory) == false) + { + foreach (var file in toRemove) + { + try + { + File.Delete(Path.Combine(_tempPath, file)); + } + catch (IOException ex) + { + LogHelper.Error("Could not delete index file, could not sync from main storage", ex); + //quit here + return false; + } + } + } + else + { + LogHelper.Warn("Cannot sync index files from main storage, the index is currently locked"); + //quit here + return false; + } + } + + foreach (var fileName in allSnapshotFiles.Where(f => f.IsNullOrWhiteSpace() == false)) + { + try + { + File.Copy( + Path.Combine(basePath, "Index", fileName), + Path.Combine(_tempPath, Path.GetFileName(fileName)), true); + } + catch (IOException ex) + { + LogHelper.Error("Could not copy index file, could not sync from main storage", ex); + + //quit here + return false; + } + } + + } + finally + { + Snapshotter.Release(); + } + } + + return true; + } + } + } +} \ No newline at end of file diff --git a/src/UmbracoExamine/LocalStorage/MultiIndexOutput.cs b/src/UmbracoExamine/LocalStorage/MultiIndexOutput.cs new file mode 100644 index 0000000000..fed13643b8 --- /dev/null +++ b/src/UmbracoExamine/LocalStorage/MultiIndexOutput.cs @@ -0,0 +1,72 @@ +using System; +using System.Linq; +using Lucene.Net.Store; + +namespace UmbracoExamine.LocalStorage +{ + public class MultiIndexOutput : IndexOutput + { + private readonly IndexOutput[] _outputs; + + public MultiIndexOutput(params IndexOutput[] outputs) + { + if (outputs.Length < 1) + { + throw new InvalidOperationException("There must be at least one output specified"); + } + _outputs = outputs; + } + + public override void WriteByte(byte b) + { + foreach (var output in _outputs) + { + output.WriteByte(b); + } + } + + public override void WriteBytes(byte[] b, int offset, int length) + { + foreach (var output in _outputs) + { + output.WriteBytes(b, offset, length); + } + } + + public override void Flush() + { + foreach (var output in _outputs) + { + output.Flush(); + } + } + + public override void Close() + { + foreach (var output in _outputs) + { + output.Close(); + } + } + + public override long GetFilePointer() + { + //return the first + return _outputs.First().GetFilePointer(); + } + + public override void Seek(long pos) + { + foreach (var output in _outputs) + { + output.Seek(pos); + } + } + + public override long Length() + { + //return the first + return _outputs.First().GetFilePointer(); + } + } +} \ No newline at end of file diff --git a/src/UmbracoExamine/UmbracoContentIndexer.cs b/src/UmbracoExamine/UmbracoContentIndexer.cs index 3c23784d56..d154c748a4 100644 --- a/src/UmbracoExamine/UmbracoContentIndexer.cs +++ b/src/UmbracoExamine/UmbracoContentIndexer.cs @@ -12,6 +12,7 @@ using Examine; using Examine.Config; using Examine.Providers; using Lucene.Net.Documents; +using Lucene.Net.Index; using Umbraco.Core; using umbraco.cms.businesslogic; using Umbraco.Core.Models; @@ -25,6 +26,7 @@ using Examine.LuceneEngine.Providers; using Lucene.Net.Analysis; using umbraco.BasePages; using IContentService = Umbraco.Core.Services.IContentService; +using UmbracoExamine.LocalStorage; using IMediaService = Umbraco.Core.Services.IMediaService; @@ -120,6 +122,8 @@ namespace UmbracoExamine #region Constants & Fields + private readonly LocalTempStorageIndexer _localTempStorageHelper = new LocalTempStorageIndexer(); + /// /// Used to store the path of a content object /// @@ -180,7 +184,7 @@ namespace UmbracoExamine /// /// An attempt is made to call on a provider after the provider has already been initialized. /// - + public override void Initialize(string name, NameValueCollection config) { @@ -203,6 +207,22 @@ namespace UmbracoExamine base.Initialize(name, config); + + if (config != null && config["useTempStorage"] != null) + { + //Use the temp storage directory which will store the index in the local/codegen folder, this is useful + // for websites that are running from a remove file server and file IO latency becomes an issue + var attemptUseTempStorage = config["useTempStorage"].TryConvertTo(); + if (attemptUseTempStorage) + { + var indexSet = IndexSets.Instance.Sets[IndexSetName]; + var configuredPath = indexSet.IndexPath; + + _localTempStorageHelper.Initialize(config, configuredPath, base.GetLuceneDirectory(), IndexingAnalyzer); + } + } + + } #endregion @@ -276,6 +296,33 @@ namespace UmbracoExamine #region Public methods + public override Lucene.Net.Store.Directory GetLuceneDirectory() + { + //if temp local storage is configured use that, otherwise return the default + if (_localTempStorageHelper.LuceneDirectory != null) + { + return _localTempStorageHelper.LuceneDirectory; + } + + return base.GetLuceneDirectory(); + + } + + public override IndexWriter GetIndexWriter() + { + //if temp local storage is configured use that, otherwise return the default + if (_localTempStorageHelper.LuceneDirectory != null) + { + return new IndexWriter(GetLuceneDirectory(), IndexingAnalyzer, + //create the writer with the snapshotter, though that won't make too much a difference because we are not keeping the writer open unless using nrt + // which we are not currently. + _localTempStorageHelper.Snapshotter, + IndexWriter.MaxFieldLength.UNLIMITED); + } + + return base.GetIndexWriter(); + } + /// /// Overridden for logging diff --git a/src/UmbracoExamine/UmbracoExamine.csproj b/src/UmbracoExamine/UmbracoExamine.csproj index e64821b098..55099b0bcd 100644 --- a/src/UmbracoExamine/UmbracoExamine.csproj +++ b/src/UmbracoExamine/UmbracoExamine.csproj @@ -121,6 +121,10 @@ + + + + diff --git a/src/UmbracoExamine/UmbracoExamineSearcher.cs b/src/UmbracoExamine/UmbracoExamineSearcher.cs index ba9e9a19d8..f0fe4c8681 100644 --- a/src/UmbracoExamine/UmbracoExamineSearcher.cs +++ b/src/UmbracoExamine/UmbracoExamineSearcher.cs @@ -2,15 +2,19 @@ using System.IO; using System.Linq; using System.Security; +using System.Web; using Examine; +using Examine.LuceneEngine.Config; using Examine.Providers; using Examine.SearchCriteria; +using Lucene.Net.Store; using Umbraco.Core; using UmbracoExamine.Config; using Examine.LuceneEngine; using Examine.LuceneEngine.Providers; using Examine.LuceneEngine.SearchCriteria; using Lucene.Net.Analysis; +using UmbracoExamine.LocalStorage; namespace UmbracoExamine @@ -21,6 +25,10 @@ namespace UmbracoExamine public class UmbracoExamineSearcher : LuceneSearcher { + private volatile Lucene.Net.Store.Directory _localTempDirectory; + private static readonly object Locker = new object(); + private string _localTempPath = null; + #region Constructors /// @@ -54,12 +62,26 @@ namespace UmbracoExamine _name = name; //We need to check if we actually can initialize, if not then don't continue - if (!CanInitialize()) + if (CanInitialize() == false) { return; } base.Initialize(name, config); + + if (config != null && config["useTempStorage"] != null) + { + //Use the temp storage directory which will store the index in the local/codegen folder, this is useful + // for websites that are running from a remove file server and file IO latency becomes an issue + var attemptUseTempStorage = config["useTempStorage"].TryConvertTo(); + if (attemptUseTempStorage) + { + var indexSet = IndexSets.Instance.Sets[IndexSetName]; + var configuredPath = indexSet.IndexPath; + var codegenPath = HttpRuntime.CodegenDir; + _localTempPath = Path.Combine(codegenPath, configuredPath.TrimStart('~', '/').Replace("/", "\\")); + } + } } /// @@ -136,6 +158,28 @@ namespace UmbracoExamine .Where(x => x != UmbracoContentIndexer.IndexPathFieldName) .Where(x => x != UmbracoContentIndexer.NodeTypeAliasFieldName) .ToArray(); - } + } + + protected override Lucene.Net.Store.Directory GetLuceneDirectory() + { + //local temp storage is not enabled, just return the default + if (_localTempPath == null) return base.GetLuceneDirectory(); + + //local temp storage is enabled, configure the local directory instance + if (_localTempDirectory == null) + { + lock (Locker) + { + if (_localTempDirectory == null) + { + _localTempDirectory = LocalTempStorageDirectoryTracker.Current.GetDirectory( + new DirectoryInfo(_localTempPath), + base.GetLuceneDirectory()); + } + } + } + + return _localTempDirectory; + } } } diff --git a/src/UmbracoExamine/UmbracoMemberIndexer.cs b/src/UmbracoExamine/UmbracoMemberIndexer.cs index 9f3496fb6d..6ab7e6d9fd 100644 --- a/src/UmbracoExamine/UmbracoMemberIndexer.cs +++ b/src/UmbracoExamine/UmbracoMemberIndexer.cs @@ -1,16 +1,13 @@ using System; -using System.Collections; +using System.Collections.Specialized; using System.Linq; -using System.Security; using System.Xml.Linq; -using System.Xml.XPath; using Examine.LuceneEngine.Config; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Services; using UmbracoExamine.Config; -using Examine.LuceneEngine; using System.Collections.Generic; using Examine; using System.IO; @@ -73,7 +70,9 @@ namespace UmbracoExamine _memberService = memberService; } - /// + + + /// /// Ensures that the'_searchEmail' is added to the user fields so that it is indexed - without having to modify the config /// /// 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 diff --git a/src/umbraco.sln b/src/umbraco.sln index bd080d6f97..70a87f184c 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2013 -VisualStudioVersion = 12.0.30723.0 +VisualStudioVersion = 12.0.31101.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web.UI", "Umbraco.Web.UI\Umbraco.Web.UI.csproj", "{4C4C194C-B5E4-4991-8F87-4373E24CC19F}" EndProject