From b251d23a576445db53739297af7489f35f3f28e6 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 30 May 2017 15:33:13 +0200 Subject: [PATCH] Refactor DI and type loading --- src/Umbraco.Compat7/Compat7Component.cs | 4 +- src/Umbraco.Core/CoreRuntime.cs | 8 +- src/Umbraco.Core/CoreRuntimeComponent.cs | 13 +- .../ConfigurationCompositionRoot.cs | 22 + .../CoreModelMappersCompositionRoot.cs | 2 +- .../RepositoryCompositionRoot.cs | 2 +- .../ServicesCompositionRoot.cs | 2 +- .../DI/ConfigurationCompositionRoot.cs | 22 - src/Umbraco.Core/DI/Current.cs | 88 +- .../DI/LazyCollectionBuilderBase.cs | 3 + src/Umbraco.Core/DI/LightInjectExtensions.cs | 55 +- .../DI/OrderedCollectionBuilderBase.cs | 4 + .../DI/WeightedCollectionBuilderBase.cs | 4 + src/Umbraco.Core/Media/IImageUrlProvider.cs | 2 +- src/Umbraco.Core/Media/IThumbnailProvider.cs | 2 +- .../PublishedContentModelFactory.cs | 2 +- .../Repositories/AuditRepository.cs | 1 + .../Repositories/MigrationEntryRepository.cs | 1 + .../Repositories/RelationRepository.cs | 1 + .../Repositories/TaskRepository.cs | 1 + .../Repositories/TaskTypeRepository.cs | 1 + .../{PluginManager.cs => TypeLoader.cs} | 1559 ++++++++--------- .../Plugins/TypeLoaderExtensions.cs | 78 + src/Umbraco.Core/ServiceProviderExtensions.cs | 2 +- src/Umbraco.Core/Udi.cs | 3 +- .../ActionCollectionTests.cs | 2 +- .../PackageActionCollectionTests.cs | 2 +- .../DependencyInjection/ResolverBaseTest.cs | 4 +- .../XsltExtensionsResolverTests.cs | 2 +- .../Migrations/Upgrades/BaseUpgradeTest.cs | 2 - .../Plugins/PluginManagerExtensions.cs | 4 +- .../Plugins/PluginManagerTests.cs | 30 +- .../PublishedContentMoreTests.cs | 100 +- .../PublishedContent/PublishedContentTests.cs | 34 +- .../Routing/RenderRouteHandlerTests.cs | 4 +- .../TestHelpers/BaseUsingSqlCeSyntax.cs | 4 +- .../TestHelpers/TestWithDatabaseBase.cs | 2 +- .../Testing/UmbracoTestAttribute.cs | 15 +- src/Umbraco.Tests/Testing/UmbracoTestBase.cs | 51 +- .../Testing/UmbracoTestOptions.cs | 10 + src/Umbraco.Tests/unit-test-log4net.config | 2 +- src/Umbraco.Web/Current.cs | 2 +- .../Editors/BackOfficeController.cs | 2 +- src/Umbraco.Web/LightInjectExtensions.cs | 20 +- src/Umbraco.Web/Macros/XsltMacroEngine.cs | 2 +- src/Umbraco.Web/PluginManagerExtensions.cs | 32 +- .../Services/ApplicationTreeService.cs | 2 +- src/Umbraco.Web/Services/SectionService.cs | 2 +- .../Migrations/PostMigrationComponent.cs | 2 +- src/Umbraco.Web/WebRuntimeComponent.cs | 6 +- .../umbraco/Trees/TreeDefinitionCollection.cs | 2 +- 51 files changed, 1109 insertions(+), 1113 deletions(-) create mode 100644 src/Umbraco.Core/DI/CompositionRoots/ConfigurationCompositionRoot.cs rename src/Umbraco.Core/DI/{ => CompositionRoots}/CoreModelMappersCompositionRoot.cs (86%) rename src/Umbraco.Core/DI/{ => CompositionRoots}/RepositoryCompositionRoot.cs (99%) rename src/Umbraco.Core/DI/{ => CompositionRoots}/ServicesCompositionRoot.cs (99%) delete mode 100644 src/Umbraco.Core/DI/ConfigurationCompositionRoot.cs rename src/Umbraco.Core/Plugins/{PluginManager.cs => TypeLoader.cs} (70%) create mode 100644 src/Umbraco.Core/Plugins/TypeLoaderExtensions.cs diff --git a/src/Umbraco.Compat7/Compat7Component.cs b/src/Umbraco.Compat7/Compat7Component.cs index e4f2300ee7..edcbd018f2 100644 --- a/src/Umbraco.Compat7/Compat7Component.cs +++ b/src/Umbraco.Compat7/Compat7Component.cs @@ -26,8 +26,8 @@ namespace Umbraco.Compat7 _app = container.GetInstance(); var logger = container.GetInstance(); - var pluginManager = container.GetInstance(); - var handlerTypes = pluginManager.ResolveTypes(); + var pluginManager = container.GetInstance(); + var handlerTypes = pluginManager.GetTypes(); _handlers = handlerTypes.Select(Activator.CreateInstance).Cast().ToList(); diff --git a/src/Umbraco.Core/CoreRuntime.cs b/src/Umbraco.Core/CoreRuntime.cs index acbc137a72..9ede68364d 100644 --- a/src/Umbraco.Core/CoreRuntime.cs +++ b/src/Umbraco.Core/CoreRuntime.cs @@ -40,8 +40,7 @@ namespace Umbraco.Core /// The Umbraco HttpApplication. public CoreRuntime(UmbracoApplicationBase umbracoApplication) { - if (umbracoApplication == null) throw new ArgumentNullException(nameof(umbracoApplication)); - _app = umbracoApplication; + _app = umbracoApplication ?? throw new ArgumentNullException(nameof(umbracoApplication)); } /// @@ -209,7 +208,7 @@ namespace Umbraco.Core container.RegisterSingleton(f => f.GetInstance().RuntimeCache); // register the plugin manager - container.RegisterSingleton(f => new PluginManager(f.GetInstance(), f.GetInstance())); + container.RegisterSingleton(f => new TypeLoader(f.GetInstance(), f.GetInstance())); // register syntax providers - required by database factory container.Register("MySqlSyntaxProvider"); @@ -378,7 +377,8 @@ namespace Umbraco.Core // getters can be implemented by runtimes inheriting from CoreRuntime - protected virtual IEnumerable GetComponentTypes() => Current.PluginManager.ResolveTypes(); + // fixme - inject! no Current! + protected virtual IEnumerable GetComponentTypes() => Current.TypeLoader.GetTypes(); // by default, returns null, meaning that Umbraco should auto-detect the application root path. // override and return the absolute path to the Umbraco site/solution, if needed diff --git a/src/Umbraco.Core/CoreRuntimeComponent.cs b/src/Umbraco.Core/CoreRuntimeComponent.cs index 0496b83fcd..d77decfb07 100644 --- a/src/Umbraco.Core/CoreRuntimeComponent.cs +++ b/src/Umbraco.Core/CoreRuntimeComponent.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Components; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.DI; +using Umbraco.Core.DI.CompositionRoots; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; @@ -62,10 +63,10 @@ namespace Umbraco.Core composition.Container.RegisterSingleton(); composition.Container.RegisterCollectionBuilder() - .Add(factory => factory.GetInstance().ResolvePropertyEditors()); + .Add(factory => factory.GetInstance().GetPropertyEditors()); composition.Container.RegisterCollectionBuilder() - .Add(factory => factory.GetInstance().ResolveParameterEditors()); + .Add(factory => factory.GetInstance().GetParameterEditors()); // register our predefined validators composition.Container.RegisterCollectionBuilder() @@ -102,17 +103,17 @@ namespace Umbraco.Core true, new DatabaseServerMessengerOptions())); composition.Container.RegisterCollectionBuilder() - .Add(factory => factory.GetInstance().ResolveCacheRefreshers()); + .Add(factory => factory.GetInstance().GetCacheRefreshers()); composition.Container.RegisterCollectionBuilder() - .Add(f => f.GetInstance().ResolvePackageActions()); + .Add(f => f.GetInstance().GetPackageActions()); composition.Container.RegisterCollectionBuilder() - .Add(factory => factory.GetInstance().ResolveTypes()); + .Add(factory => factory.GetInstance().GetTypes()); // need to filter out the ones we dont want!! fixme - what does that mean? composition.Container.RegisterCollectionBuilder() - .Append(factory => factory.GetInstance().ResolveTypes()); + .Append(factory => factory.GetInstance().GetTypes()); composition.Container.RegisterSingleton(factory => new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault(factory.GetInstance()))); diff --git a/src/Umbraco.Core/DI/CompositionRoots/ConfigurationCompositionRoot.cs b/src/Umbraco.Core/DI/CompositionRoots/ConfigurationCompositionRoot.cs new file mode 100644 index 0000000000..141a3f770b --- /dev/null +++ b/src/Umbraco.Core/DI/CompositionRoots/ConfigurationCompositionRoot.cs @@ -0,0 +1,22 @@ +using LightInject; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; + +namespace Umbraco.Core.DI.CompositionRoots +{ + /// + /// Sets up IoC container for Umbraco configuration classes + /// + public sealed class ConfigurationCompositionRoot : ICompositionRoot + { + public void Compose(IServiceRegistry container) + { + container.Register(factory => UmbracoConfig.For.UmbracoSettings()); + container.Register(factory => factory.GetInstance().Content); + container.Register(factory => factory.GetInstance().Templates); + container.Register(factory => factory.GetInstance().RequestHandler); + + // fixme - other sections we need to add? + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/DI/CoreModelMappersCompositionRoot.cs b/src/Umbraco.Core/DI/CompositionRoots/CoreModelMappersCompositionRoot.cs similarity index 86% rename from src/Umbraco.Core/DI/CoreModelMappersCompositionRoot.cs rename to src/Umbraco.Core/DI/CompositionRoots/CoreModelMappersCompositionRoot.cs index 41b1834567..ee0fefec82 100644 --- a/src/Umbraco.Core/DI/CoreModelMappersCompositionRoot.cs +++ b/src/Umbraco.Core/DI/CompositionRoots/CoreModelMappersCompositionRoot.cs @@ -1,7 +1,7 @@ using LightInject; using Umbraco.Core.Models.Identity; -namespace Umbraco.Core.DI +namespace Umbraco.Core.DI.CompositionRoots { public sealed class CoreModelMappersCompositionRoot : ICompositionRoot { diff --git a/src/Umbraco.Core/DI/RepositoryCompositionRoot.cs b/src/Umbraco.Core/DI/CompositionRoots/RepositoryCompositionRoot.cs similarity index 99% rename from src/Umbraco.Core/DI/RepositoryCompositionRoot.cs rename to src/Umbraco.Core/DI/CompositionRoots/RepositoryCompositionRoot.cs index 8bf9ae0513..755da337bf 100644 --- a/src/Umbraco.Core/DI/RepositoryCompositionRoot.cs +++ b/src/Umbraco.Core/DI/CompositionRoots/RepositoryCompositionRoot.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; -namespace Umbraco.Core.DI +namespace Umbraco.Core.DI.CompositionRoots { /// /// Sets the IoC container for the umbraco data layer/repositories/sql/database/etc... diff --git a/src/Umbraco.Core/DI/ServicesCompositionRoot.cs b/src/Umbraco.Core/DI/CompositionRoots/ServicesCompositionRoot.cs similarity index 99% rename from src/Umbraco.Core/DI/ServicesCompositionRoot.cs rename to src/Umbraco.Core/DI/CompositionRoots/ServicesCompositionRoot.cs index 25be1c75e1..11042dce90 100644 --- a/src/Umbraco.Core/DI/ServicesCompositionRoot.cs +++ b/src/Umbraco.Core/DI/CompositionRoots/ServicesCompositionRoot.cs @@ -8,7 +8,7 @@ using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Services; -namespace Umbraco.Core.DI +namespace Umbraco.Core.DI.CompositionRoots { public sealed class ServicesCompositionRoot : ICompositionRoot { diff --git a/src/Umbraco.Core/DI/ConfigurationCompositionRoot.cs b/src/Umbraco.Core/DI/ConfigurationCompositionRoot.cs deleted file mode 100644 index 335c8ed573..0000000000 --- a/src/Umbraco.Core/DI/ConfigurationCompositionRoot.cs +++ /dev/null @@ -1,22 +0,0 @@ -using LightInject; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; - -namespace Umbraco.Core.DI -{ - /// - /// Sets up IoC container for Umbraco configuration classes - /// - public sealed class ConfigurationCompositionRoot : ICompositionRoot - { - public void Compose(IServiceRegistry container) - { - container.Register(factory => UmbracoConfig.For.UmbracoSettings()); - container.Register(factory => factory.GetInstance().Content); - container.Register(factory => factory.GetInstance().Templates); - container.Register(factory => factory.GetInstance().RequestHandler); - - //TODO: Add the other config areas... - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/DI/Current.cs b/src/Umbraco.Core/DI/Current.cs index b0fff220da..dbbf6dc3d2 100644 --- a/src/Umbraco.Core/DI/Current.cs +++ b/src/Umbraco.Core/DI/Current.cs @@ -17,15 +17,27 @@ using Umbraco.Core._Legacy.PackageActions; namespace Umbraco.Core.DI { - // this class is here to support the transition from singletons and resolvers to injection, - // by providing a static access to singleton services - it is initialized once with a service - // container, in CoreBootManager. - // obviously, this is some sort of service locator anti-pattern. ideally, it should not exist. - // practically... time will tell. + /// + /// Provides a static service locator for most singletons. + /// + /// + /// This class is initialized with the container via LightInjectExtensions.ConfigureUmbracoCore, + /// right after the container is created in UmbracoApplicationBase.HandleApplicationStart. + /// Obviously, this is a service locator, which some may consider an anti-pattern. And yet, + /// practically, it works. + /// public static class Current { private static ServiceContainer _container; + private static IShortStringHelper _shortStringHelper; + private static ILogger _logger; + private static IProfiler _profiler; + private static ProfilingLogger _profilingLogger; + + /// + /// Gets or sets the DI container. + /// internal static ServiceContainer Container { get @@ -52,7 +64,6 @@ namespace Umbraco.Core.DI _logger = null; _profiler = null; _profilingLogger = null; - _pluginManager = null; // fixme - some of our tests don't reset it? Resetted?.Invoke(null, EventArgs.Empty); } @@ -61,22 +72,33 @@ namespace Umbraco.Core.DI #region Getters + // fixme - refactor + // we don't want Umbraco to die because the container has not been properly initialized, + // for some too-important things such as IShortStringHelper or loggers, so if it's not + // registered we setup a default one. We should really refactor our tests so that it does + // not happen. Will do when we get rid of IShortStringHelper. + + public static IShortStringHelper ShortStringHelper + => _shortStringHelper ?? (_shortStringHelper = _container?.TryGetInstance() + ?? new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault(UmbracoConfig.For.UmbracoSettings()))); + + public static ILogger Logger + => _logger ?? (_logger = _container?.TryGetInstance() + ?? new DebugDiagnosticsLogger()); + + public static IProfiler Profiler + => _profiler ?? (_profiler = _container?.TryGetInstance() + ?? new LogProfiler(Logger)); + + public static ProfilingLogger ProfilingLogger + => _profilingLogger ?? (_profilingLogger = _container?.TryGetInstance()) + ?? new ProfilingLogger(Logger, Profiler); + public static IRuntimeState RuntimeState => Container.GetInstance(); - // fixme - refactor - // some of our tests did mess with the current plugin manager - // so for the time being we support it, however we should fix our tests - - private static PluginManager _pluginManager; - - public static PluginManager PluginManager - { - get { return _pluginManager - ?? (_pluginManager = Container.TryGetInstance() - ?? new PluginManager(ApplicationCache.RuntimeCache, ProfilingLogger)); } - set { _pluginManager = value; } - } + public static TypeLoader TypeLoader + => Container.GetInstance(); public static FileSystems FileSystems => Container.GetInstance(); @@ -114,34 +136,6 @@ namespace Umbraco.Core.DI public static ICultureDictionaryFactory CultureDictionaryFactory => Container.GetInstance(); - // fixme - refactor - // we don't want Umbraco to die because the container has not been properly initialized, - // for some too-important things such as IShortStringHelper or loggers, so if it's not - // registered we setup a default one. We should really refactor our tests so that it does - // not happen, but hey... - - private static IShortStringHelper _shortStringHelper; - - public static IShortStringHelper ShortStringHelper - => _shortStringHelper ?? (_shortStringHelper = _container?.TryGetInstance() - ?? new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault(UmbracoConfig.For.UmbracoSettings()))); - - private static ILogger _logger; - private static IProfiler _profiler; - private static ProfilingLogger _profilingLogger; - - public static ILogger Logger - => _logger ?? (_logger = _container?.TryGetInstance() - ?? new DebugDiagnosticsLogger()); - - public static IProfiler Profiler - => _profiler ?? (_profiler = _container?.TryGetInstance() - ?? new LogProfiler(Logger)); - - public static ProfilingLogger ProfilingLogger - => _profilingLogger ?? (_profilingLogger = _container?.TryGetInstance()) - ?? new ProfilingLogger(Logger, Profiler); - public static CacheHelper ApplicationCache => Container.GetInstance(); diff --git a/src/Umbraco.Core/DI/LazyCollectionBuilderBase.cs b/src/Umbraco.Core/DI/LazyCollectionBuilderBase.cs index 764a179f37..2c821a972c 100644 --- a/src/Umbraco.Core/DI/LazyCollectionBuilderBase.cs +++ b/src/Umbraco.Core/DI/LazyCollectionBuilderBase.cs @@ -19,6 +19,9 @@ namespace Umbraco.Core.DI private readonly List>> _producers2 = new List>>(); private readonly List _excluded = new List(); + /// + /// Initializes a new instance of the class. + /// protected LazyCollectionBuilderBase(IServiceContainer container) : base(container) { } diff --git a/src/Umbraco.Core/DI/LightInjectExtensions.cs b/src/Umbraco.Core/DI/LightInjectExtensions.cs index 370717c5f0..ff5973d962 100644 --- a/src/Umbraco.Core/DI/LightInjectExtensions.cs +++ b/src/Umbraco.Core/DI/LightInjectExtensions.cs @@ -186,7 +186,7 @@ namespace Umbraco.Core.DI UpdateRegistration(registration, null, factory); } - // fixme - what's below ALSO applies to non-singleton ie transient services + // note - what's below ALSO applies to non-singleton ie transient services // // see https://github.com/seesharper/LightInject/issues/133 // @@ -201,6 +201,9 @@ namespace Umbraco.Core.DI // all in all, not sure we want to let ppl have direct access to the container // we might instead want to expose some methods in UmbracoComponentBase or whatever? + /// + /// Updates a registration. + /// private static void UpdateRegistration(Registration registration, Type implementingType, Delegate factoryExpression) { // if the container has compiled already then the registrations have been captured, @@ -315,56 +318,6 @@ namespace Umbraco.Core.DI } } - // FIXME or just use names?! - // this is what RegisterMany does => kill RegisterCollection! - - /// - /// In order for LightInject to deal with enumerables of the same type, each one needs to be registered as their explicit types - /// - /// - /// - /// - /// - /// This works as of 3.0.2.2: https://github.com/seesharper/LightInject/issues/68#issuecomment-70611055 - /// but means that the explicit type is registered, not the implementing type - /// - public static void RegisterCollection(this IServiceContainer container, IEnumerable implementationTypes) - where TLifetime : ILifetime, new() - { - foreach (var type in implementationTypes) - container.Register(type, new TLifetime()); - } - - public static void RegisterCollection(this IServiceContainer container, Func> implementationTypes) - where TLifetime : ILifetime, new() - { - foreach (var type in implementationTypes(container)) - container.Register(type, new TLifetime()); - } - - /// - /// In order for LightInject to deal with enumerables of the same type, each one needs to be registered as their explicit types - /// - /// - /// - /// - /// This works as of 3.0.2.2: https://github.com/seesharper/LightInject/issues/68#issuecomment-70611055 - /// but means that the explicit type is registered, not the implementing type - /// - public static void RegisterCollection(this IServiceContainer container, IEnumerable implementationTypes) - { - foreach (var type in implementationTypes) - { - container.Register(type); - } - } - - public static void RegisterCollection(this IServiceContainer container, Func> implementationTypes) - { - foreach (var type in implementationTypes(container)) - container.Register(type); - } - /// /// Registers a base type for auto-registration. /// diff --git a/src/Umbraco.Core/DI/OrderedCollectionBuilderBase.cs b/src/Umbraco.Core/DI/OrderedCollectionBuilderBase.cs index 1ae3ed1363..97aca7fc0c 100644 --- a/src/Umbraco.Core/DI/OrderedCollectionBuilderBase.cs +++ b/src/Umbraco.Core/DI/OrderedCollectionBuilderBase.cs @@ -14,6 +14,10 @@ namespace Umbraco.Core.DI where TBuilder : OrderedCollectionBuilderBase where TCollection : IBuilderCollection { + /// + /// Initializes a new instance of the class. + /// + /// protected OrderedCollectionBuilderBase(IServiceContainer container) : base (container) { } diff --git a/src/Umbraco.Core/DI/WeightedCollectionBuilderBase.cs b/src/Umbraco.Core/DI/WeightedCollectionBuilderBase.cs index 53287bb988..87c9952141 100644 --- a/src/Umbraco.Core/DI/WeightedCollectionBuilderBase.cs +++ b/src/Umbraco.Core/DI/WeightedCollectionBuilderBase.cs @@ -15,6 +15,10 @@ namespace Umbraco.Core.DI where TBuilder : WeightedCollectionBuilderBase where TCollection : IBuilderCollection { + /// + /// Initializes a new instance of the class. + /// + /// protected WeightedCollectionBuilderBase(IServiceContainer container) : base(container) { } diff --git a/src/Umbraco.Core/Media/IImageUrlProvider.cs b/src/Umbraco.Core/Media/IImageUrlProvider.cs index 29c0ae34ed..f5e62baae9 100644 --- a/src/Umbraco.Core/Media/IImageUrlProvider.cs +++ b/src/Umbraco.Core/Media/IImageUrlProvider.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Umbraco.Core.Media { // note: because this interface is obsolete is is *not* IDiscoverable, and in case the - // PluginManager is asked to find types implementing this interface it will fall back + // TypeLoader is asked to find types implementing this interface it will fall back // to a complete scan. [Obsolete("IImageUrlProvider is no longer used and will be removed in future versions")] diff --git a/src/Umbraco.Core/Media/IThumbnailProvider.cs b/src/Umbraco.Core/Media/IThumbnailProvider.cs index 18b8453324..0a9192b251 100644 --- a/src/Umbraco.Core/Media/IThumbnailProvider.cs +++ b/src/Umbraco.Core/Media/IThumbnailProvider.cs @@ -3,7 +3,7 @@ namespace Umbraco.Core.Media { // note: because this interface is obsolete is is *not* IDiscoverable, and in case the - // PluginManager is asked to find types implementing this interface it will fall back + // TypeLoader is asked to find types implementing this interface it will fall back // to a complete scan. [Obsolete("Thumbnails are generated by ImageProcessor, use that instead")] diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactory.cs index d3d6b43170..c43e49e473 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactory.cs @@ -23,7 +23,7 @@ namespace Umbraco.Core.Models.PublishedContent /// accepts one IPublishedContent as a parameter. /// To activate, /// - /// var types = PluginManager.Current.ResolveTypes{PublishedContentModel}(); + /// var types = TypeLoader.Current.GetTypes{PublishedContentModel}(); /// var factory = new PublishedContentModelFactoryImpl(types); /// PublishedContentModelFactoryResolver.Current.SetFactory(factory); /// diff --git a/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs index eeff23c877..86b8ae4d27 100644 --- a/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/AuditRepository.cs @@ -5,6 +5,7 @@ using LightInject; using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.DI; +using Umbraco.Core.DI.CompositionRoots; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; diff --git a/src/Umbraco.Core/Persistence/Repositories/MigrationEntryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MigrationEntryRepository.cs index 5e27025923..e065db4b8b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MigrationEntryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MigrationEntryRepository.cs @@ -6,6 +6,7 @@ using NPoco; using Semver; using Umbraco.Core.Cache; using Umbraco.Core.DI; +using Umbraco.Core.DI.CompositionRoots; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; diff --git a/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs index 104e5c41c5..a6baff1b7c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs @@ -5,6 +5,7 @@ using LightInject; using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.DI; +using Umbraco.Core.DI.CompositionRoots; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; diff --git a/src/Umbraco.Core/Persistence/Repositories/TaskRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TaskRepository.cs index 1abe5606d6..e99e3a2662 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TaskRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TaskRepository.cs @@ -5,6 +5,7 @@ using LightInject; using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.DI; +using Umbraco.Core.DI.CompositionRoots; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; diff --git a/src/Umbraco.Core/Persistence/Repositories/TaskTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TaskTypeRepository.cs index 36869583b7..8a96c77ae6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TaskTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TaskTypeRepository.cs @@ -5,6 +5,7 @@ using LightInject; using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.DI; +using Umbraco.Core.DI.CompositionRoots; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; diff --git a/src/Umbraco.Core/Plugins/PluginManager.cs b/src/Umbraco.Core/Plugins/TypeLoader.cs similarity index 70% rename from src/Umbraco.Core/Plugins/PluginManager.cs rename to src/Umbraco.Core/Plugins/TypeLoader.cs index dc9a45f47b..3413a27843 100644 --- a/src/Umbraco.Core/Plugins/PluginManager.cs +++ b/src/Umbraco.Core/Plugins/TypeLoader.cs @@ -1,815 +1,744 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Web.Compilation; -using Umbraco.Core.Cache; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Persistence.Mappers; -using Umbraco.Core.Persistence.SqlSyntax; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core._Legacy.PackageActions; -using File = System.IO.File; - -namespace Umbraco.Core.Plugins -{ - /// - /// Provides methods to find and instanciate types. - /// - /// - /// This class should be used to resolve all types, the class should never be used directly. - /// In most cases this class is not used directly but through extension methods that retrieve specific types. - /// This class caches the types it knows to avoid excessive assembly scanning and shorten startup times, relying - /// on a hash of the DLLs in the ~/bin folder to check for cache expiration. - /// - public class PluginManager - { - private const string CacheKey = "umbraco-plugins.list"; - - private readonly IRuntimeCacheProvider _runtimeCache; - private readonly ProfilingLogger _logger; - private readonly string _tempFolder; - - private readonly object _typesLock = new object(); - private readonly Dictionary _types = new Dictionary(); - - private long _cachedAssembliesHash = -1; - private long _currentAssembliesHash = -1; - private IEnumerable _assemblies; - private bool _reportedChange; - - /// - /// Initializes a new instance of the class. - /// - /// The application runtime cache. - /// A profiling logger. - /// Whether to detect changes using hashes. - public PluginManager(IRuntimeCacheProvider runtimeCache, ProfilingLogger logger, bool detectChanges = true) - { - if (runtimeCache == null) throw new ArgumentNullException(nameof(runtimeCache)); - if (logger == null) throw new ArgumentNullException(nameof(logger)); - - _runtimeCache = runtimeCache; - _logger = logger; - - // the temp folder where the cache file lives - _tempFolder = IOHelper.MapPath("~/App_Data/TEMP/PluginCache"); - if (Directory.Exists(_tempFolder) == false) - Directory.CreateDirectory(_tempFolder); - - var pluginListFile = GetPluginListFilePath(); - - if (detectChanges) - { - //first check if the cached hash is 0, if it is then we ne - //do the check if they've changed - RequiresRescanning = (CachedAssembliesHash != CurrentAssembliesHash) || CachedAssembliesHash == 0; - //if they have changed, we need to write the new file - if (RequiresRescanning) - { - // if the hash has changed, clear out the persisted list no matter what, this will force - // rescanning of all plugin types including lazy ones. - // http://issues.umbraco.org/issue/U4-4789 - File.Delete(pluginListFile); - - WriteCachePluginsHash(); - } - } - else - { - // if the hash has changed, clear out the persisted list no matter what, this will force - // rescanning of all plugin types including lazy ones. - // http://issues.umbraco.org/issue/U4-4789 - File.Delete(pluginListFile); - - // always set to true if we're not detecting (generally only for testing) - RequiresRescanning = true; - } - } - - /// - /// Gets or sets the set of assemblies to scan. - /// - /// - /// If not explicitely set, defaults to all assemblies except those that are know to not have any of the - /// types we might scan. Because we only scan for application types, this means we can safely exclude GAC assemblies - /// for example. - /// This is for unit tests. - /// - internal IEnumerable AssembliesToScan - { - get { return _assemblies ?? (_assemblies = TypeFinder.GetAssembliesWithKnownExclusions()); } - set { _assemblies = value; } - } - - /// - /// Gets the type lists. - /// - /// For unit tests. - internal IEnumerable TypeLists => _types.Values; - - /// - /// Sets a type list. - /// - /// For unit tests. - internal void AddTypeList(TypeList typeList) - { - _types[new TypeListKey(typeList.BaseType, typeList.AttributeType)] = typeList; - } - - #region Hashing - - /// - /// Gets a value indicating whether the assemblies in bin, app_code, global.asax, etc... have changed since they were last hashed. - /// - internal bool RequiresRescanning { get; } - - /// - /// Gets the currently cached hash value of the scanned assemblies. - /// - /// The cached hash value, or 0 if no cache is found. - internal long CachedAssembliesHash - { - get - { - if (_cachedAssembliesHash != -1) - return _cachedAssembliesHash; - - var filePath = GetPluginHashFilePath(); - if (File.Exists(filePath) == false) return 0; - - var hash = File.ReadAllText(filePath, Encoding.UTF8); - - long val; - if (long.TryParse(hash, out val) == false) return 0; - - _cachedAssembliesHash = val; - return _cachedAssembliesHash; - } - } - - /// - /// Gets the current assemblies hash based on creating a hash from the assemblies in various places. - /// - /// The current hash. - internal long CurrentAssembliesHash - { - get - { - if (_currentAssembliesHash != -1) - return _currentAssembliesHash; - - _currentAssembliesHash = GetFileHash(new List> - { - // the bin folder and everything in it - new Tuple(new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Bin)), false), - // the app code folder and everything in it - new Tuple(new DirectoryInfo(IOHelper.MapPath("~/App_Code")), false), - // global.asax (the app domain also monitors this, if it changes will do a full restart) - new Tuple(new FileInfo(IOHelper.MapPath("~/global.asax")), false), - // trees.config - use the contents to create the hash since this gets resaved on every app startup! - new Tuple(new FileInfo(IOHelper.MapPath(SystemDirectories.Config + "/trees.config")), true) - }, _logger); - - return _currentAssembliesHash; - } - } - - /// - /// Writes the assembly hash file. - /// - private void WriteCachePluginsHash() - { - var filePath = GetPluginHashFilePath(); - File.WriteAllText(filePath, CurrentAssembliesHash.ToString(), Encoding.UTF8); - } - - /// - /// Returns a unique hash for a combination of FileInfo objects. - /// - /// A collection of files. - /// A profiling logger. - /// The hash. - /// Each file is a tuple containing the FileInfo object and a boolean which indicates whether to hash the - /// file properties (false) or the file contents (true). - internal static long GetFileHash(IEnumerable> filesAndFolders, ProfilingLogger logger) - { - using (logger.TraceDuration("Determining hash of code files on disk", "Hash determined")) - { - var hashCombiner = new HashCodeCombiner(); - - // get the distinct file infos to hash - var uniqInfos = new HashSet(); - var uniqContent = new HashSet(); - - foreach (var fileOrFolder in filesAndFolders) - { - var info = fileOrFolder.Item1; - if (fileOrFolder.Item2) - { - // add each unique file's contents to the hash - // normalize the content for cr/lf and case-sensitivity - - if (uniqContent.Contains(info.FullName)) continue; - uniqContent.Add(info.FullName); - if (File.Exists(info.FullName) == false) continue; - var content = RemoveCrLf(File.ReadAllText(info.FullName)); - hashCombiner.AddCaseInsensitiveString(content); - } - else - { - // add each unique folder/file to the hash - - if (uniqInfos.Contains(info.FullName)) continue; - uniqInfos.Add(info.FullName); - hashCombiner.AddFileSystemItem(info); - } - } - - return ConvertHashToInt64(hashCombiner.GetCombinedHashCode()); - } - } - - // fast! (yes, according to benchmarks) - private static string RemoveCrLf(string s) - { - var buffer = new char[s.Length]; - var count = 0; - // ReSharper disable once ForCanBeConvertedToForeach - no! - for (var i = 0; i < s.Length; i++) - { - if (s[i] != '\r' && s[i] != '\n') - buffer[count++] = s[i]; - } - return new string(buffer, 0, count); - } - - /// - /// Returns a unique hash for a combination of FileInfo objects. - /// - /// A collection of files. - /// A profiling logger. - /// The hash. - internal static long GetFileHash(IEnumerable filesAndFolders, ProfilingLogger logger) - { - using (logger.TraceDuration("Determining hash of code files on disk", "Hash determined")) - { - var hashCombiner = new HashCodeCombiner(); - - // get the distinct file infos to hash - var uniqInfos = new HashSet(); - - foreach (var fileOrFolder in filesAndFolders) - { - if (uniqInfos.Contains(fileOrFolder.FullName)) continue; - uniqInfos.Add(fileOrFolder.FullName); - hashCombiner.AddFileSystemItem(fileOrFolder); - } - - return ConvertHashToInt64(hashCombiner.GetCombinedHashCode()); - } - } - - /// - /// Converts a string hash value into an Int64. - /// - internal static long ConvertHashToInt64(string val) - { - long outVal; - return long.TryParse(val, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out outVal) ? outVal : 0; - } - - #endregion - - #region Cache - - /// - /// Attemps to retrieve the list of types from the cache. - /// - /// Fails if the cache is missing or corrupt in any way. - internal Attempt> TryGetCached(Type baseType, Type attributeType) - { - var cache = _runtimeCache.GetCacheItem, IEnumerable>>(CacheKey, ReadCacheSafe, TimeSpan.FromMinutes(4)); - - IEnumerable types; - cache.TryGetValue(Tuple.Create(baseType == null ? string.Empty : baseType.FullName, attributeType == null ? string.Empty : attributeType.FullName), out types); - return types == null - ? Attempt>.Fail() - : Attempt.Succeed(types); - } - - internal Dictionary, IEnumerable> ReadCacheSafe() - { - try - { - return ReadCache(); - } - catch - { - try - { - var filePath = GetPluginListFilePath(); - File.Delete(filePath); - } - catch - { - // on-purpose, does not matter - } - } - - return new Dictionary, IEnumerable>(); - } - - internal Dictionary, IEnumerable> ReadCache() - { - var cache = new Dictionary, IEnumerable>(); - - var filePath = GetPluginListFilePath(); - if (File.Exists(filePath) == false) - return cache; - - using (var stream = File.OpenRead(filePath)) - using (var reader = new StreamReader(stream)) - { - while (true) - { - var baseType = reader.ReadLine(); - if (baseType == null) return cache; // exit - if (baseType.StartsWith("<")) break; // old xml - - var attributeType = reader.ReadLine(); - if (attributeType == null) break; - - var types = new List(); - while (true) - { - var type = reader.ReadLine(); - if (type == null) - { - types = null; // break 2 levels - break; - } - if (type == string.Empty) - { - cache[Tuple.Create(baseType, attributeType)] = types; - break; - } - types.Add(type); - } - - if (types == null) break; - } - } - - cache.Clear(); - return cache; - } - - /// - /// Removes cache files and internal cache. - /// - /// Generally only used for resetting cache, for example during the install process. - public void ClearPluginCache() - { - var path = GetPluginListFilePath(); - if (File.Exists(path)) - File.Delete(path); - - path = GetPluginHashFilePath(); - if (File.Exists(path)) - File.Delete(path); - - _runtimeCache.ClearCacheItem(CacheKey); - } - - private string GetPluginListFilePath() - { - var filename = "umbraco-plugins." + NetworkHelper.FileSafeMachineName + ".list"; - return Path.Combine(_tempFolder, filename); - } - - private string GetPluginHashFilePath() - { - var filename = "umbraco-plugins." + NetworkHelper.FileSafeMachineName + ".hash"; - return Path.Combine(_tempFolder, filename); - } - - internal void WriteCache() - { - var filePath = GetPluginListFilePath(); - - using (var stream = File.Open(filePath, FileMode.Create, FileAccess.ReadWrite)) - using (var writer = new StreamWriter(stream)) - { - foreach (var typeList in _types.Values) - { - writer.WriteLine(typeList.BaseType == null ? string.Empty : typeList.BaseType.FullName); - writer.WriteLine(typeList.AttributeType == null ? string.Empty : typeList.AttributeType.FullName); - foreach (var type in typeList.Types) - writer.WriteLine(type.AssemblyQualifiedName); - writer.WriteLine(); - } - } - } - - internal void UpdateCache() - { - // note - // at the moment we write the cache to disk every time we update it. ideally we defer the writing - // since all the updates are going to happen in a row when Umbraco starts. that being said, the - // file is small enough, so it is not a priority. - WriteCache(); - } - - #endregion - - #region Resolve Types - - /// - /// Resolves class types inheriting from or implementing the specified type - /// - /// The type to inherit from or implement. - /// Indicates whether to use cache for type resolution. - /// A set of assemblies for type resolution. - /// All class types inheriting from or implementing the specified type. - /// Caching is disabled when using specific assemblies. - public IEnumerable ResolveTypes(bool cache = true, IEnumerable specificAssemblies = null) - { - // do not cache anything from specific assemblies - cache &= specificAssemblies == null; - - // if not caching, or not IDiscoverable, directly resolve types - if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false) - { - return ResolveTypesInternal( - typeof (T), null, - () => TypeFinder.FindClassesOfType(specificAssemblies ?? AssembliesToScan), - cache); - } - - // if caching and IDiscoverable - // filter the cached discovered types (and cache the result) - - var discovered = ResolveTypesInternal( - typeof (IDiscoverable), null, - () => TypeFinder.FindClassesOfType(AssembliesToScan), - true); - - return ResolveTypesInternal( - typeof (T), null, - () => discovered - .Where(x => typeof (T).IsAssignableFrom(x)), - true); - } - - /// - /// Resolves class types inheriting from or implementing the specified type and marked with the specified attribute. - /// - /// The type to inherit from or implement. - /// The type of the attribute. - /// Indicates whether to use cache for type resolution. - /// A set of assemblies for type resolution. - /// All class types inheriting from or implementing the specified type and marked with the specified attribute. - /// Caching is disabled when using specific assemblies. - public IEnumerable ResolveTypesWithAttribute(bool cache = true, IEnumerable specificAssemblies = null) - where TAttribute : Attribute - { - // do not cache anything from specific assemblies - cache &= specificAssemblies == null; - - // if not caching, or not IDiscoverable, directly resolve types - if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false) - { - return ResolveTypesInternal( - typeof (T), typeof (TAttribute), - () => TypeFinder.FindClassesOfTypeWithAttribute(specificAssemblies ?? AssembliesToScan), - cache); - } - - // if caching and IDiscoverable - // filter the cached discovered types (and cache the result) - - var discovered = ResolveTypesInternal( - typeof (IDiscoverable), null, - () => TypeFinder.FindClassesOfType(AssembliesToScan), - true); - - return ResolveTypesInternal( - typeof (T), typeof (TAttribute), - () => discovered - .Where(x => typeof(T).IsAssignableFrom(x)) - .Where(x => x.GetCustomAttributes(false).Any()), - true); - } - - /// - /// Resolves class types marked with the specified attribute. - /// - /// The type of the attribute. - /// Indicates whether to use cache for type resolution. - /// A set of assemblies for type resolution. - /// All class types marked with the specified attribute. - /// Caching is disabled when using specific assemblies. - public IEnumerable ResolveAttributedTypes(bool cache = true, IEnumerable specificAssemblies = null) - where TAttribute : Attribute - { - // do not cache anything from specific assemblies - cache &= specificAssemblies == null; - - return ResolveTypesInternal( - typeof (object), typeof (TAttribute), - () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), - cache); - } - - private IEnumerable ResolveTypesInternal( - Type baseType, Type attributeType, - Func> finder, - bool cache) - { - // using an upgradeable lock makes little sense here as only one thread can enter the upgradeable - // lock at a time, and we don't have non-upgradeable readers, and quite probably the plugin - // manager is mostly not going to be used in any kind of massively multi-threaded scenario - so, - // a plain lock is enough - - var name = ResolvedName(baseType, attributeType); - - lock (_typesLock) - using (_logger.TraceDuration( - "Resolving " + name, - "Resolved " + name)) // cannot contain typesFound.Count as it's evaluated before the find - { - // resolve within a lock & timer - return ResolveTypesInternalLocked(baseType, attributeType, finder, cache); - } - } - - private static string ResolvedName(Type baseType, Type attributeType) - { - var s = attributeType == null ? string.Empty : ("[" + attributeType + "]"); - s += baseType; - return s; - } - - private IEnumerable ResolveTypesInternalLocked( - Type baseType, Type attributeType, - Func> finder, - bool cache) - { - // check if the TypeList already exists, if so return it, if not we'll create it - var listKey = new TypeListKey(baseType, attributeType); - TypeList typeList = null; - if (cache) - _types.TryGetValue(listKey, out typeList); // else null - - // if caching and found, return - if (typeList != null) - { - // need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505 - _logger.Logger.Debug("Resolving {0}: found a cached type list.", () => ResolvedName(baseType, attributeType)); - return typeList.Types; - } - - // else proceed, - typeList = new TypeList(baseType, attributeType); - - var scan = RequiresRescanning || File.Exists(GetPluginListFilePath()) == false; - - if (scan) - { - // either we have to rescan, or we could not find the cache file: - // report (only once) and scan and update the cache file - if (_reportedChange == false) - { - _logger.Logger.Debug("Assemblies changes detected, need to rescan everything."); - _reportedChange = true; - } - } - - if (scan == false) - { - // if we don't have to scan, try the cache - var cacheResult = TryGetCached(baseType, attributeType); - - // here we need to identify if the CachedPluginNotFoundInFile was the exception, if it was then we need to re-scan - // in some cases the plugin will not have been scanned for on application startup, but the assemblies haven't changed - // so in this instance there will never be a result. - if (cacheResult.Exception is CachedPluginNotFoundInFileException || cacheResult.Success == false) - { - _logger.Logger.Debug("Resolving {0}: failed to load from cache file, must scan assemblies.", () => ResolvedName(baseType, attributeType)); - scan = true; - } - else - { - // successfully retrieved types from the file cache: load - foreach (var type in cacheResult.Result) - { - try - { - // we use the build manager to ensure we get all types loaded, this is slightly slower than - // Type.GetType but if the types in the assembly aren't loaded yet it would fail whereas - // BuildManager will load them - this is how eg MVC loads types, etc - no need to make it - // more complicated - typeList.Add(BuildManager.GetType(type, true)); - } - catch (Exception ex) - { - // in case of any exception, we have to exit, and revert to scanning - _logger.Logger.Error("Resolving " + ResolvedName(baseType, attributeType) + ": failed to load cache file type " + type + ", reverting to scanning assemblies.", ex); - scan = true; - break; - } - } - - if (scan == false) - { - _logger.Logger.Debug("Resolving {0}: loaded types from cache file.", () => ResolvedName(baseType, attributeType)); - } - } - } - - if (scan) - { - // either we had to scan, or we could not resolve the types from the cache file - scan now - _logger.Logger.Debug("Resolving {0}: scanning assemblies.", () => ResolvedName(baseType, attributeType)); - - foreach (var t in finder()) - typeList.Add(t); - } - - // if we are to cache the results, do so - if (cache) - { - var added = _types.ContainsKey(listKey) == false; - if (added) - { - _types[listKey] = typeList; - //if we are scanning then update the cache file - if (scan) - UpdateCache(); - } - - _logger.Logger.Debug("Resolved {0}, caching ({1}).", () => ResolvedName(baseType, attributeType), () => added.ToString().ToLowerInvariant()); - } - else - { - _logger.Logger.Debug("Resolved {0}.", () => ResolvedName(baseType, attributeType)); - } - - return typeList.Types; - } - - #endregion - - #region Nested classes and stuff - - /// - /// Groups a type and a resolution kind into a key. - /// - private struct TypeListKey - { - // ReSharper disable MemberCanBePrivate.Local - public readonly Type BaseType; - public readonly Type AttributeType; - // ReSharper restore MemberCanBePrivate.Local - - public TypeListKey(Type baseType, Type attributeType) - { - BaseType = baseType ?? typeof (object); - AttributeType = attributeType; - } - - public override bool Equals(object obj) - { - if (obj == null || obj is TypeListKey == false) return false; - var o = (TypeListKey)obj; - return BaseType == o.BaseType && AttributeType == o.AttributeType; - } - - public override int GetHashCode() - { - // in case AttributeType is null we need something else, using typeof (TypeListKey) - // which does not really "mean" anything, it's just a value... - - var hash = 5381; - hash = ((hash << 5) + hash) ^ BaseType.GetHashCode(); - hash = ((hash << 5) + hash) ^ (AttributeType ?? typeof (TypeListKey)).GetHashCode(); - return hash; - } - } - - /// - /// Represents a list of types obtained by looking for types inheriting/implementing a - /// specified type, and/or marked with a specified attribute type. - /// - internal class TypeList - { - private readonly HashSet _types = new HashSet(); - - public TypeList(Type baseType, Type attributeType) - { - BaseType = baseType; - AttributeType = attributeType; - } - - public Type BaseType { get; } - public Type AttributeType { get; } - - /// - /// Adds a type. - /// - public void Add(Type type) - { - if (BaseType.IsAssignableFrom(type) == false) - throw new ArgumentException("Base type " + BaseType + " is not assignable from type " + type + ".", "type"); - _types.Add(type); - } - - /// - /// Gets the types. - /// - public IEnumerable Types => _types; - } - - /// - /// Represents the error that occurs when a plugin was not found in the cache plugin - /// list with the specified TypeResolutionKind. - /// - internal class CachedPluginNotFoundInFileException : Exception - { } - - #endregion - } - - // fixme - extract! - internal static class PluginManagerExtensions - { - /// - /// Gets all classes inheriting from PropertyEditor. - /// - /// - /// Excludes the actual PropertyEditor base type. - /// - public static IEnumerable ResolvePropertyEditors(this PluginManager mgr) - { - // look for IParameterEditor (fast, IDiscoverable) then filter - - var propertyEditor = typeof (PropertyEditor); - - return mgr.ResolveTypes() - .Where(x => propertyEditor.IsAssignableFrom(x) && x != propertyEditor); - } - - /// - /// Gets all classes implementing IParameterEditor. - /// - /// - /// Includes property editors. - /// Excludes the actual ParameterEditor and PropertyEditor base types. - /// - public static IEnumerable ResolveParameterEditors(this PluginManager mgr) - { - var propertyEditor = typeof (PropertyEditor); - var parameterEditor = typeof (ParameterEditor); - - return mgr.ResolveTypes() - .Where(x => x != propertyEditor && x != parameterEditor); - } - - /// - /// Gets all classes implementing ICacheRefresher. - /// - public static IEnumerable ResolveCacheRefreshers(this PluginManager mgr) - { - return mgr.ResolveTypes(); - } - - /// - /// Gets all classes implementing IPackageAction. - /// - public static IEnumerable ResolvePackageActions(this PluginManager mgr) - { - return mgr.ResolveTypes(); - } - - /// - /// Gets all classes inheriting from BaseMapper and marked with the MapperForAttribute. - /// - public static IEnumerable ResolveAssignedMapperTypes(this PluginManager mgr) - { - return mgr.ResolveTypesWithAttribute(); - } - - /// - /// Gets all classes implementing ISqlSyntaxProvider and marked with the SqlSyntaxProviderAttribute. - /// - public static IEnumerable ResolveSqlSyntaxProviders(this PluginManager mgr) - { - return mgr.ResolveTypesWithAttribute(); - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Web.Compilation; +using Umbraco.Core.Cache; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using File = System.IO.File; + +namespace Umbraco.Core.Plugins +{ + /// + /// Provides methods to find and instanciate types. + /// + /// + /// This class should be used to get all types, the class should never be used directly. + /// In most cases this class is not used directly but through extension methods that retrieve specific types. + /// This class caches the types it knows to avoid excessive assembly scanning and shorten startup times, relying + /// on a hash of the DLLs in the ~/bin folder to check for cache expiration. + /// + public class TypeLoader + { + private const string CacheKey = "umbraco-types.list"; + + private readonly IRuntimeCacheProvider _runtimeCache; + private readonly ProfilingLogger _logger; + private readonly string _tempFolder; + + private readonly object _typesLock = new object(); + private readonly Dictionary _types = new Dictionary(); + + private long _cachedAssembliesHash = -1; + private long _currentAssembliesHash = -1; + private IEnumerable _assemblies; + private bool _reportedChange; + + /// + /// Initializes a new instance of the class. + /// + /// The application runtime cache. + /// A profiling logger. + /// Whether to detect changes using hashes. + internal TypeLoader(IRuntimeCacheProvider runtimeCache, ProfilingLogger logger, bool detectChanges = true) + { + _runtimeCache = runtimeCache ?? throw new ArgumentNullException(nameof(runtimeCache)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + // the temp folder where the cache file lives + _tempFolder = IOHelper.MapPath("~/App_Data/TEMP/TypesCache"); + if (Directory.Exists(_tempFolder) == false) + Directory.CreateDirectory(_tempFolder); + + var typesListFile = GeTypesListFilePath(); + + if (detectChanges) + { + //first check if the cached hash is 0, if it is then we ne + //do the check if they've changed + RequiresRescanning = (CachedAssembliesHash != CurrentAssembliesHash) || CachedAssembliesHash == 0; + //if they have changed, we need to write the new file + if (RequiresRescanning) + { + // if the hash has changed, clear out the persisted list no matter what, this will force + // rescanning of all types including lazy ones. + // http://issues.umbraco.org/issue/U4-4789 + File.Delete(typesListFile); + + WriteCacheTypesHash(); + } + } + else + { + // if the hash has changed, clear out the persisted list no matter what, this will force + // rescanning of all types including lazy ones. + // http://issues.umbraco.org/issue/U4-4789 + File.Delete(typesListFile); + + // always set to true if we're not detecting (generally only for testing) + RequiresRescanning = true; + } + } + + /// + /// Gets or sets the set of assemblies to scan. + /// + /// + /// If not explicitely set, defaults to all assemblies except those that are know to not have any of the + /// types we might scan. Because we only scan for application types, this means we can safely exclude GAC assemblies + /// for example. + /// This is for unit tests. + /// + // internal for tests + internal IEnumerable AssembliesToScan + { + get => _assemblies ?? (_assemblies = TypeFinder.GetAssembliesWithKnownExclusions()); + set => _assemblies = value; + } + + /// + /// Gets the type lists. + /// + /// For unit tests. + // internal for tests + internal IEnumerable TypeLists => _types.Values; + + /// + /// Sets a type list. + /// + /// For unit tests. + // internal for tests + internal void AddTypeList(TypeList typeList) + { + _types[new TypeListKey(typeList.BaseType, typeList.AttributeType)] = typeList; + } + + #region Hashing + + /// + /// Gets a value indicating whether the assemblies in bin, app_code, global.asax, etc... have changed since they were last hashed. + /// + private bool RequiresRescanning { get; } + + /// + /// Gets the currently cached hash value of the scanned assemblies. + /// + /// The cached hash value, or 0 if no cache is found. + private long CachedAssembliesHash + { + get + { + if (_cachedAssembliesHash != -1) + return _cachedAssembliesHash; + + var filePath = GetTypesHashFilePath(); + if (File.Exists(filePath) == false) return 0; + + var hash = File.ReadAllText(filePath, Encoding.UTF8); + + long val; + if (long.TryParse(hash, out val) == false) return 0; + + _cachedAssembliesHash = val; + return _cachedAssembliesHash; + } + } + + /// + /// Gets the current assemblies hash based on creating a hash from the assemblies in various places. + /// + /// The current hash. + private long CurrentAssembliesHash + { + get + { + if (_currentAssembliesHash != -1) + return _currentAssembliesHash; + + _currentAssembliesHash = GetFileHash(new List> + { + // the bin folder and everything in it + new Tuple(new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Bin)), false), + // the app code folder and everything in it + new Tuple(new DirectoryInfo(IOHelper.MapPath("~/App_Code")), false), + // global.asax (the app domain also monitors this, if it changes will do a full restart) + new Tuple(new FileInfo(IOHelper.MapPath("~/global.asax")), false), + // trees.config - use the contents to create the hash since this gets resaved on every app startup! + new Tuple(new FileInfo(IOHelper.MapPath(SystemDirectories.Config + "/trees.config")), true) + }, _logger); + + return _currentAssembliesHash; + } + } + + /// + /// Writes the assembly hash file. + /// + private void WriteCacheTypesHash() + { + var filePath = GetTypesHashFilePath(); + File.WriteAllText(filePath, CurrentAssembliesHash.ToString(), Encoding.UTF8); + } + + /// + /// Returns a unique hash for a combination of FileInfo objects. + /// + /// A collection of files. + /// A profiling logger. + /// The hash. + /// Each file is a tuple containing the FileInfo object and a boolean which indicates whether to hash the + /// file properties (false) or the file contents (true). + private static long GetFileHash(IEnumerable> filesAndFolders, ProfilingLogger logger) + { + using (logger.TraceDuration("Determining hash of code files on disk", "Hash determined")) + { + var hashCombiner = new HashCodeCombiner(); + + // get the distinct file infos to hash + var uniqInfos = new HashSet(); + var uniqContent = new HashSet(); + + foreach (var fileOrFolder in filesAndFolders) + { + var info = fileOrFolder.Item1; + if (fileOrFolder.Item2) + { + // add each unique file's contents to the hash + // normalize the content for cr/lf and case-sensitivity + + if (uniqContent.Contains(info.FullName)) continue; + uniqContent.Add(info.FullName); + if (File.Exists(info.FullName) == false) continue; + var content = RemoveCrLf(File.ReadAllText(info.FullName)); + hashCombiner.AddCaseInsensitiveString(content); + } + else + { + // add each unique folder/file to the hash + + if (uniqInfos.Contains(info.FullName)) continue; + uniqInfos.Add(info.FullName); + hashCombiner.AddFileSystemItem(info); + } + } + + return ConvertHashToInt64(hashCombiner.GetCombinedHashCode()); + } + } + + // fast! (yes, according to benchmarks) + private static string RemoveCrLf(string s) + { + var buffer = new char[s.Length]; + var count = 0; + // ReSharper disable once ForCanBeConvertedToForeach - no! + for (var i = 0; i < s.Length; i++) + { + if (s[i] != '\r' && s[i] != '\n') + buffer[count++] = s[i]; + } + return new string(buffer, 0, count); + } + + /// + /// Returns a unique hash for a combination of FileInfo objects. + /// + /// A collection of files. + /// A profiling logger. + /// The hash. + // internal for tests + internal static long GetFileHash(IEnumerable filesAndFolders, ProfilingLogger logger) + { + using (logger.TraceDuration("Determining hash of code files on disk", "Hash determined")) + { + var hashCombiner = new HashCodeCombiner(); + + // get the distinct file infos to hash + var uniqInfos = new HashSet(); + + foreach (var fileOrFolder in filesAndFolders) + { + if (uniqInfos.Contains(fileOrFolder.FullName)) continue; + uniqInfos.Add(fileOrFolder.FullName); + hashCombiner.AddFileSystemItem(fileOrFolder); + } + + return ConvertHashToInt64(hashCombiner.GetCombinedHashCode()); + } + } + + /// + /// Converts a string hash value into an Int64. + /// + // internal for tests + internal static long ConvertHashToInt64(string val) + { + long outVal; + return long.TryParse(val, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out outVal) ? outVal : 0; + } + + #endregion + + #region Cache + + // internal for tests + internal Attempt> TryGetCached(Type baseType, Type attributeType) + { + var cache = _runtimeCache.GetCacheItem, IEnumerable>>(CacheKey, ReadCacheSafe, TimeSpan.FromMinutes(4)); + + cache.TryGetValue(Tuple.Create(baseType == null ? string.Empty : baseType.FullName, attributeType == null ? string.Empty : attributeType.FullName), out IEnumerable types); + return types == null + ? Attempt>.Fail() + : Attempt.Succeed(types); + } + + private Dictionary, IEnumerable> ReadCacheSafe() + { + try + { + return ReadCache(); + } + catch + { + try + { + var filePath = GeTypesListFilePath(); + File.Delete(filePath); + } + catch + { + // on-purpose, does not matter + } + } + + return new Dictionary, IEnumerable>(); + } + + // internal for tests + internal Dictionary, IEnumerable> ReadCache() + { + var cache = new Dictionary, IEnumerable>(); + + var filePath = GeTypesListFilePath(); + if (File.Exists(filePath) == false) + return cache; + + using (var stream = File.OpenRead(filePath)) + using (var reader = new StreamReader(stream)) + { + while (true) + { + var baseType = reader.ReadLine(); + if (baseType == null) return cache; // exit + if (baseType.StartsWith("<")) break; // old xml + + var attributeType = reader.ReadLine(); + if (attributeType == null) break; + + var types = new List(); + while (true) + { + var type = reader.ReadLine(); + if (type == null) + { + types = null; // break 2 levels + break; + } + if (type == string.Empty) + { + cache[Tuple.Create(baseType, attributeType)] = types; + break; + } + types.Add(type); + } + + if (types == null) break; + } + } + + cache.Clear(); + return cache; + } + + private string GeTypesListFilePath() + { + var filename = "umbraco-types." + NetworkHelper.FileSafeMachineName + ".list"; + return Path.Combine(_tempFolder, filename); + } + + private string GetTypesHashFilePath() + { + var filename = "umbraco-types." + NetworkHelper.FileSafeMachineName + ".hash"; + return Path.Combine(_tempFolder, filename); + } + + // internal for tests + internal void WriteCache() + { + var filePath = GeTypesListFilePath(); + + using (var stream = File.Open(filePath, FileMode.Create, FileAccess.ReadWrite)) + using (var writer = new StreamWriter(stream)) + { + foreach (var typeList in _types.Values) + { + writer.WriteLine(typeList.BaseType == null ? string.Empty : typeList.BaseType.FullName); + writer.WriteLine(typeList.AttributeType == null ? string.Empty : typeList.AttributeType.FullName); + foreach (var type in typeList.Types) + writer.WriteLine(type.AssemblyQualifiedName); + writer.WriteLine(); + } + } + } + + // internal for tests + internal void UpdateCache() + { + // note + // at the moment we write the cache to disk every time we update it. ideally we defer the writing + // since all the updates are going to happen in a row when Umbraco starts. that being said, the + // file is small enough, so it is not a priority. + WriteCache(); + } + + /// + /// Removes cache files and internal cache. + /// + /// Generally only used for resetting cache, for example during the install process. + public void ClearTypesCache() + { + var path = GeTypesListFilePath(); + if (File.Exists(path)) + File.Delete(path); + + path = GetTypesHashFilePath(); + if (File.Exists(path)) + File.Delete(path); + + _runtimeCache.ClearCacheItem(CacheKey); + } + + #endregion + + #region Get Types + + /// + /// Gets class types inheriting from or implementing the specified type + /// + /// The type to inherit from or implement. + /// Indicates whether to use cache for type resolution. + /// A set of assemblies for type resolution. + /// All class types inheriting from or implementing the specified type. + /// Caching is disabled when using specific assemblies. + public IEnumerable GetTypes(bool cache = true, IEnumerable specificAssemblies = null) + { + // do not cache anything from specific assemblies + cache &= specificAssemblies == null; + + // if not caching, or not IDiscoverable, directly get types + if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false) + { + return GetTypesInternal( + typeof (T), null, + () => TypeFinder.FindClassesOfType(specificAssemblies ?? AssembliesToScan), + cache); + } + + // if caching and IDiscoverable + // filter the cached discovered types (and cache the result) + + var discovered = GetTypesInternal( + typeof (IDiscoverable), null, + () => TypeFinder.FindClassesOfType(AssembliesToScan), + true); + + return GetTypesInternal( + typeof (T), null, + () => discovered + .Where(x => typeof (T).IsAssignableFrom(x)), + true); + } + + /// + /// Gets class types inheriting from or implementing the specified type and marked with the specified attribute. + /// + /// The type to inherit from or implement. + /// The type of the attribute. + /// Indicates whether to use cache for type resolution. + /// A set of assemblies for type resolution. + /// All class types inheriting from or implementing the specified type and marked with the specified attribute. + /// Caching is disabled when using specific assemblies. + public IEnumerable GetTypesWithAttribute(bool cache = true, IEnumerable specificAssemblies = null) + where TAttribute : Attribute + { + // do not cache anything from specific assemblies + cache &= specificAssemblies == null; + + // if not caching, or not IDiscoverable, directly get types + if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false) + { + return GetTypesInternal( + typeof (T), typeof (TAttribute), + () => TypeFinder.FindClassesOfTypeWithAttribute(specificAssemblies ?? AssembliesToScan), + cache); + } + + // if caching and IDiscoverable + // filter the cached discovered types (and cache the result) + + var discovered = GetTypesInternal( + typeof (IDiscoverable), null, + () => TypeFinder.FindClassesOfType(AssembliesToScan), + true); + + return GetTypesInternal( + typeof (T), typeof (TAttribute), + () => discovered + .Where(x => typeof(T).IsAssignableFrom(x)) + .Where(x => x.GetCustomAttributes(false).Any()), + true); + } + + /// + /// Gets class types marked with the specified attribute. + /// + /// The type of the attribute. + /// Indicates whether to use cache for type resolution. + /// A set of assemblies for type resolution. + /// All class types marked with the specified attribute. + /// Caching is disabled when using specific assemblies. + public IEnumerable GetAttributedTypes(bool cache = true, IEnumerable specificAssemblies = null) + where TAttribute : Attribute + { + // do not cache anything from specific assemblies + cache &= specificAssemblies == null; + + return GetTypesInternal( + typeof (object), typeof (TAttribute), + () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), + cache); + } + + private IEnumerable GetTypesInternal( + Type baseType, Type attributeType, + Func> finder, + bool cache) + { + // using an upgradeable lock makes little sense here as only one thread can enter the upgradeable + // lock at a time, and we don't have non-upgradeable readers, and quite probably the type + // loader is mostly not going to be used in any kind of massively multi-threaded scenario - so, + // a plain lock is enough + + var name = GetName(baseType, attributeType); + + lock (_typesLock) + using (_logger.TraceDuration( + "Getting " + name, + "Got " + name)) // cannot contain typesFound.Count as it's evaluated before the find + { + // get within a lock & timer + return GetTypesInternalLocked(baseType, attributeType, finder, cache); + } + } + + private static string GetName(Type baseType, Type attributeType) + { + var s = attributeType == null ? string.Empty : ("[" + attributeType + "]"); + s += baseType; + return s; + } + + private IEnumerable GetTypesInternalLocked( + Type baseType, Type attributeType, + Func> finder, + bool cache) + { + // check if the TypeList already exists, if so return it, if not we'll create it + var listKey = new TypeListKey(baseType, attributeType); + TypeList typeList = null; + if (cache) + _types.TryGetValue(listKey, out typeList); // else null + + // if caching and found, return + if (typeList != null) + { + // need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505 + _logger.Logger.Debug("Getting {0}: found a cached type list.", () => GetName(baseType, attributeType)); + return typeList.Types; + } + + // else proceed, + typeList = new TypeList(baseType, attributeType); + + var scan = RequiresRescanning || File.Exists(GeTypesListFilePath()) == false; + + if (scan) + { + // either we have to rescan, or we could not find the cache file: + // report (only once) and scan and update the cache file + if (_reportedChange == false) + { + _logger.Logger.Debug("Assemblies changes detected, need to rescan everything."); + _reportedChange = true; + } + } + + if (scan == false) + { + // if we don't have to scan, try the cache + var cacheResult = TryGetCached(baseType, attributeType); + + // here we need to identify if the CachedTypeNotFoundInFile was the exception, if it was then we need to re-scan + // in some cases the type will not have been scanned for on application startup, but the assemblies haven't changed + // so in this instance there will never be a result. + if (cacheResult.Exception is CachedTypeNotFoundInFileException || cacheResult.Success == false) + { + _logger.Logger.Debug("Getting {0}: failed to load from cache file, must scan assemblies.", () => GetName(baseType, attributeType)); + scan = true; + } + else + { + // successfully retrieved types from the file cache: load + foreach (var type in cacheResult.Result) + { + try + { + // we use the build manager to ensure we get all types loaded, this is slightly slower than + // Type.GetType but if the types in the assembly aren't loaded yet it would fail whereas + // BuildManager will load them - this is how eg MVC loads types, etc - no need to make it + // more complicated + typeList.Add(BuildManager.GetType(type, true)); + } + catch (Exception ex) + { + // in case of any exception, we have to exit, and revert to scanning + _logger.Logger.Error("Getting " + GetName(baseType, attributeType) + ": failed to load cache file type " + type + ", reverting to scanning assemblies.", ex); + scan = true; + break; + } + } + + if (scan == false) + { + _logger.Logger.Debug("Getting {0}: loaded types from cache file.", () => GetName(baseType, attributeType)); + } + } + } + + if (scan) + { + // either we had to scan, or we could not get the types from the cache file - scan now + _logger.Logger.Debug("Getting {0}: scanning assemblies.", () => GetName(baseType, attributeType)); + + foreach (var t in finder()) + typeList.Add(t); + } + + // if we are to cache the results, do so + if (cache) + { + var added = _types.ContainsKey(listKey) == false; + if (added) + { + _types[listKey] = typeList; + //if we are scanning then update the cache file + if (scan) + UpdateCache(); + } + + _logger.Logger.Debug("Got {0}, caching ({1}).", () => GetName(baseType, attributeType), () => added.ToString().ToLowerInvariant()); + } + else + { + _logger.Logger.Debug("Got {0}.", () => GetName(baseType, attributeType)); + } + + return typeList.Types; + } + + #endregion + + #region Nested classes and stuff + + /// + /// Groups a type and a resolution kind into a key. + /// + private struct TypeListKey + { + // ReSharper disable MemberCanBePrivate.Local + public readonly Type BaseType; + public readonly Type AttributeType; + // ReSharper restore MemberCanBePrivate.Local + + public TypeListKey(Type baseType, Type attributeType) + { + BaseType = baseType ?? typeof (object); + AttributeType = attributeType; + } + + public override bool Equals(object obj) + { + if (obj == null || obj is TypeListKey == false) return false; + var o = (TypeListKey)obj; + return BaseType == o.BaseType && AttributeType == o.AttributeType; + } + + public override int GetHashCode() + { + // in case AttributeType is null we need something else, using typeof (TypeListKey) + // which does not really "mean" anything, it's just a value... + + var hash = 5381; + hash = ((hash << 5) + hash) ^ BaseType.GetHashCode(); + hash = ((hash << 5) + hash) ^ (AttributeType ?? typeof (TypeListKey)).GetHashCode(); + return hash; + } + } + + /// + /// Represents a list of types obtained by looking for types inheriting/implementing a + /// specified type, and/or marked with a specified attribute type. + /// + internal class TypeList + { + private readonly HashSet _types = new HashSet(); + + public TypeList(Type baseType, Type attributeType) + { + BaseType = baseType; + AttributeType = attributeType; + } + + public Type BaseType { get; } + public Type AttributeType { get; } + + /// + /// Adds a type. + /// + public void Add(Type type) + { + if (BaseType.IsAssignableFrom(type) == false) + throw new ArgumentException("Base type " + BaseType + " is not assignable from type " + type + ".", nameof(type)); + _types.Add(type); + } + + /// + /// Gets the types. + /// + public IEnumerable Types => _types; + } + + /// + /// Represents the error that occurs when a type was not found in the cache type + /// list with the specified TypeResolutionKind. + /// + internal class CachedTypeNotFoundInFileException : Exception + { } + + #endregion + } +} diff --git a/src/Umbraco.Core/Plugins/TypeLoaderExtensions.cs b/src/Umbraco.Core/Plugins/TypeLoaderExtensions.cs new file mode 100644 index 0000000000..9a8d4ce830 --- /dev/null +++ b/src/Umbraco.Core/Plugins/TypeLoaderExtensions.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Cache; +using Umbraco.Core.Persistence.Mappers; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core._Legacy.PackageActions; + +namespace Umbraco.Core.Plugins +{ + internal static class TypeLoaderExtensions + { + /// + /// Gets all classes inheriting from PropertyEditor. + /// + /// + /// Excludes the actual PropertyEditor base type. + /// + public static IEnumerable GetPropertyEditors(this TypeLoader mgr) + { + // look for IParameterEditor (fast, IDiscoverable) then filter + + var propertyEditor = typeof (PropertyEditor); + + return mgr.GetTypes() + .Where(x => propertyEditor.IsAssignableFrom(x) && x != propertyEditor); + } + + /// + /// Gets all classes implementing IParameterEditor. + /// + /// + /// Includes property editors. + /// Excludes the actual ParameterEditor and PropertyEditor base types. + /// + public static IEnumerable GetParameterEditors(this TypeLoader mgr) + { + var propertyEditor = typeof (PropertyEditor); + var parameterEditor = typeof (ParameterEditor); + + return mgr.GetTypes() + .Where(x => x != propertyEditor && x != parameterEditor); + } + + /// + /// Gets all classes implementing ICacheRefresher. + /// + public static IEnumerable GetCacheRefreshers(this TypeLoader mgr) + { + return mgr.GetTypes(); + } + + /// + /// Gets all classes implementing IPackageAction. + /// + public static IEnumerable GetPackageActions(this TypeLoader mgr) + { + return mgr.GetTypes(); + } + + /// + /// Gets all classes inheriting from BaseMapper and marked with the MapperForAttribute. + /// + public static IEnumerable GetAssignedMapperTypes(this TypeLoader mgr) + { + return mgr.GetTypesWithAttribute(); + } + + /// + /// Gets all classes implementing ISqlSyntaxProvider and marked with the SqlSyntaxProviderAttribute. + /// + public static IEnumerable GetSqlSyntaxProviders(this TypeLoader mgr) + { + return mgr.GetTypesWithAttribute(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/ServiceProviderExtensions.cs b/src/Umbraco.Core/ServiceProviderExtensions.cs index 8cbe6d01da..79ccb5b99b 100644 --- a/src/Umbraco.Core/ServiceProviderExtensions.cs +++ b/src/Umbraco.Core/ServiceProviderExtensions.cs @@ -32,7 +32,7 @@ namespace Umbraco.Core catch (Exception ex) { - logger.Error(String.Format("Error creating type {0}", t.FullName), ex); + logger.Error(String.Format("Error creating type {0}", t.FullName), ex); if (throwException) { diff --git a/src/Umbraco.Core/Udi.cs b/src/Umbraco.Core/Udi.cs index b3ea37ebe7..d8d6615a90 100644 --- a/src/Umbraco.Core/Udi.cs +++ b/src/Umbraco.Core/Udi.cs @@ -73,7 +73,8 @@ namespace Umbraco.Core // just pick every service connectors - just making sure that not two of them // would register the same entity type, with different udi types (would not make // much sense anyways). - var connectors = Current.PluginManager.ResolveTypes(); + // fixme - current! replace static ctor w/ component? + var connectors = Current.TypeLoader.GetTypes(); foreach (var connector in connectors) { var attrs = connector.GetCustomAttributes(false); diff --git a/src/Umbraco.Tests/DependencyInjection/ActionCollectionTests.cs b/src/Umbraco.Tests/DependencyInjection/ActionCollectionTests.cs index 9451dc07f0..3870750528 100644 --- a/src/Umbraco.Tests/DependencyInjection/ActionCollectionTests.cs +++ b/src/Umbraco.Tests/DependencyInjection/ActionCollectionTests.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests.DI public void FindAllActions() { var collectionBuilder = new ActionCollectionBuilder(); - collectionBuilder.SetProducer(() => PluginManager.ResolveActions()); + collectionBuilder.SetProducer(() => TypeLoader.ResolveActions()); var actions = collectionBuilder.CreateCollection(); Assert.AreEqual(2, actions.Count()); diff --git a/src/Umbraco.Tests/DependencyInjection/PackageActionCollectionTests.cs b/src/Umbraco.Tests/DependencyInjection/PackageActionCollectionTests.cs index d5e7fcd113..4617dbcf6a 100644 --- a/src/Umbraco.Tests/DependencyInjection/PackageActionCollectionTests.cs +++ b/src/Umbraco.Tests/DependencyInjection/PackageActionCollectionTests.cs @@ -23,7 +23,7 @@ namespace Umbraco.Tests.DI container.ConfigureUmbracoCore(); container.RegisterCollectionBuilder() - .Add(() => PluginManager.ResolvePackageActions()); + .Add(() => TypeLoader.GetPackageActions()); var actions = Current.PackageActions; Assert.AreEqual(2, actions.Count()); diff --git a/src/Umbraco.Tests/DependencyInjection/ResolverBaseTest.cs b/src/Umbraco.Tests/DependencyInjection/ResolverBaseTest.cs index 5d74da325c..0f95bb6c0f 100644 --- a/src/Umbraco.Tests/DependencyInjection/ResolverBaseTest.cs +++ b/src/Umbraco.Tests/DependencyInjection/ResolverBaseTest.cs @@ -11,7 +11,7 @@ namespace Umbraco.Tests.DI { public abstract class ResolverBaseTest // fixme rename, do something! { - protected PluginManager PluginManager { get; private set; } + protected TypeLoader TypeLoader { get; private set; } protected ProfilingLogger ProfilingLogger { get; private set; } [SetUp] @@ -19,7 +19,7 @@ namespace Umbraco.Tests.DI { ProfilingLogger = new ProfilingLogger(Mock.Of(), Mock.Of()); - PluginManager = new PluginManager(new NullCacheProvider(), + TypeLoader = new TypeLoader(new NullCacheProvider(), ProfilingLogger, false) { diff --git a/src/Umbraco.Tests/DependencyInjection/XsltExtensionsResolverTests.cs b/src/Umbraco.Tests/DependencyInjection/XsltExtensionsResolverTests.cs index 1d1d97d704..4f203d8f5a 100644 --- a/src/Umbraco.Tests/DependencyInjection/XsltExtensionsResolverTests.cs +++ b/src/Umbraco.Tests/DependencyInjection/XsltExtensionsResolverTests.cs @@ -19,7 +19,7 @@ namespace Umbraco.Tests.DI { var container = new ServiceContainer(); var builder = new XsltExtensionCollectionBuilder(container); - builder.AddExtensionObjectProducer(() => PluginManager.ResolveXsltExtensions()); + builder.AddExtensionObjectProducer(() => TypeLoader.ResolveXsltExtensions()); var extensions = builder.CreateCollection(); Assert.AreEqual(3, extensions.Count()); diff --git a/src/Umbraco.Tests/Migrations/Upgrades/BaseUpgradeTest.cs b/src/Umbraco.Tests/Migrations/Upgrades/BaseUpgradeTest.cs index 589bfe17b8..18c57ea88f 100644 --- a/src/Umbraco.Tests/Migrations/Upgrades/BaseUpgradeTest.cs +++ b/src/Umbraco.Tests/Migrations/Upgrades/BaseUpgradeTest.cs @@ -94,8 +94,6 @@ namespace Umbraco.Tests.Migrations.Upgrades [TearDown] public virtual void TearDown() { - Current.PluginManager = null; - TestHelper.CleanContentDirectories(); Path = TestHelper.CurrentAssemblyDirectory; diff --git a/src/Umbraco.Tests/Plugins/PluginManagerExtensions.cs b/src/Umbraco.Tests/Plugins/PluginManagerExtensions.cs index 8eea84a16c..47b7b4ae46 100644 --- a/src/Umbraco.Tests/Plugins/PluginManagerExtensions.cs +++ b/src/Umbraco.Tests/Plugins/PluginManagerExtensions.cs @@ -10,9 +10,9 @@ namespace Umbraco.Tests.Plugins /// internal static class PluginManagerExtensions { - public static IEnumerable ResolveFindMeTypes(this PluginManager resolver) + public static IEnumerable ResolveFindMeTypes(this TypeLoader resolver) { - return resolver.ResolveTypes(); + return resolver.GetTypes(); } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs index f45ea315d3..502df7fff0 100644 --- a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs +++ b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs @@ -28,12 +28,12 @@ namespace Umbraco.Tests.Plugins [TestFixture] public class PluginManagerTests { - private PluginManager _manager; + private TypeLoader _manager; [SetUp] public void Initialize() { //this ensures its reset - _manager = new PluginManager(new NullCacheProvider(), + _manager = new TypeLoader(new NullCacheProvider(), new ProfilingLogger(Mock.Of(), Mock.Of())); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver @@ -66,7 +66,7 @@ namespace Umbraco.Tests.Plugins private DirectoryInfo PrepareFolder() { var assDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory; - var dir = Directory.CreateDirectory(Path.Combine(assDir.FullName, "PluginManager", Guid.NewGuid().ToString("N"))); + var dir = Directory.CreateDirectory(Path.Combine(assDir.FullName, "TypeLoader", Guid.NewGuid().ToString("N"))); foreach (var f in dir.GetFiles()) { f.Delete(); @@ -77,7 +77,7 @@ namespace Umbraco.Tests.Plugins //[Test] //public void Scan_Vs_Load_Benchmark() //{ - // var pluginManager = new PluginManager(false); + // var typeLoader = new TypeLoader(false); // var watch = new Stopwatch(); // watch.Start(); // for (var i = 0; i < 1000; i++) @@ -103,7 +103,7 @@ namespace Umbraco.Tests.Plugins // watch.Start(); // for (var i = 0; i < 1000; i++) // { - // var refreshers = pluginManager.ResolveTypes(false); + // var refreshers = typeLoader.GetTypes(false); // } // watch.Stop(); // Debug.WriteLine("TOTAL TIME (2nd round): " + watch.ElapsedMilliseconds); @@ -194,9 +194,9 @@ AnotherContentFinder [Test] public void Create_Cached_Plugin_File() { - var types = new[] { typeof (PluginManager), typeof (PluginManagerTests), typeof (UmbracoContext) }; + var types = new[] { typeof (TypeLoader), typeof (PluginManagerTests), typeof (UmbracoContext) }; - var typeList1 = new PluginManager.TypeList(typeof (object), null); + var typeList1 = new TypeLoader.TypeList(typeof (object), null); foreach (var type in types) typeList1.Add(type); _manager.AddTypeList(typeList1); _manager.WriteCache(); @@ -218,7 +218,7 @@ AnotherContentFinder public void PluginHash_From_String() { var s = "hello my name is someone".GetHashCode().ToString("x", CultureInfo.InvariantCulture); - var output = PluginManager.ConvertHashToInt64(s); + var output = TypeLoader.ConvertHashToInt64(s); Assert.AreNotEqual(0, output); } @@ -250,16 +250,16 @@ AnotherContentFinder var list3 = new[] { f1, f3, f5, f7 }; //Act - var hash1 = PluginManager.GetFileHash(list1, new ProfilingLogger(Mock.Of(), Mock.Of())); - var hash2 = PluginManager.GetFileHash(list2, new ProfilingLogger(Mock.Of(), Mock.Of())); - var hash3 = PluginManager.GetFileHash(list3, new ProfilingLogger(Mock.Of(), Mock.Of())); + var hash1 = TypeLoader.GetFileHash(list1, new ProfilingLogger(Mock.Of(), Mock.Of())); + var hash2 = TypeLoader.GetFileHash(list2, new ProfilingLogger(Mock.Of(), Mock.Of())); + var hash3 = TypeLoader.GetFileHash(list3, new ProfilingLogger(Mock.Of(), Mock.Of())); //Assert Assert.AreNotEqual(hash1, hash2); Assert.AreNotEqual(hash1, hash3); Assert.AreNotEqual(hash2, hash3); - Assert.AreEqual(hash1, PluginManager.GetFileHash(list1, new ProfilingLogger(Mock.Of(), Mock.Of()))); + Assert.AreEqual(hash1, TypeLoader.GetFileHash(list1, new ProfilingLogger(Mock.Of(), Mock.Of()))); } [Test] @@ -273,7 +273,7 @@ AnotherContentFinder [Test] public void Resolves_Assigned_Mappers() { - var foundTypes1 = _manager.ResolveAssignedMapperTypes(); + var foundTypes1 = _manager.GetAssignedMapperTypes(); Assert.AreEqual(29, foundTypes1.Count()); // 29 classes in the solution implement BaseMapper } @@ -312,9 +312,9 @@ AnotherContentFinder [Test] public void TypeList_Resolves_Explicit_Types() { - var types = new HashSet(); + var types = new HashSet(); - var propEditors = new PluginManager.TypeList(typeof (PropertyEditor), null); + var propEditors = new TypeLoader.TypeList(typeof (PropertyEditor), null); propEditors.Add(typeof(LabelPropertyEditor)); types.Add(propEditors); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs index bf4699a099..90d81877bd 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs @@ -3,7 +3,6 @@ using System.Collections.ObjectModel; using System.Web.Routing; using Moq; using NUnit.Framework; -using Umbraco.Core.Cache; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web; using Umbraco.Core.Plugins; @@ -13,38 +12,35 @@ using Umbraco.Web.Security; using Umbraco.Core.DI; using Current = Umbraco.Core.DI.Current; using LightInject; +using Umbraco.Tests.Testing; namespace Umbraco.Tests.PublishedContent { [TestFixture] + [UmbracoTest(PluginManager = UmbracoTestOptions.PluginManager.PerFixture)] public class PublishedContentMoreTests : PublishedContentTestBase { - // read http://stackoverflow.com/questions/7713326/extension-method-that-works-on-ienumerablet-and-iqueryablet // and http://msmvps.com/blogs/jon_skeet/archive/2010/10/28/overloading-and-generic-constraints.aspx // and http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx - private PluginManager _pluginManager; - - public override void SetUp() - { - base.SetUp(); - - // this is so the model factory looks into the test assembly - _pluginManager = Current.PluginManager; - Current.PluginManager = new PluginManager(new NullCacheProvider(), ProfilingLogger, false) - { - AssembliesToScan = _pluginManager.AssembliesToScan - .Union(new[] { typeof (PublishedContentMoreTests).Assembly}) - }; - - InitializeUmbracoContext(); - } - protected override void Compose() { base.Compose(); - Container.RegisterSingleton(f => new PublishedContentModelFactory(f.GetInstance().ResolveTypes())); + + Container.RegisterSingleton(f => new PublishedContentModelFactory(f.GetInstance().GetTypes())); + } + + protected override TypeLoader CreatePluginManager(IServiceFactory f) + { + var pluginManager = base.CreatePluginManager(f); + + // this is so the model factory looks into the test assembly + pluginManager.AssembliesToScan = pluginManager.AssembliesToScan + .Union(new[] { typeof (PublishedContentMoreTests).Assembly }) + .ToList(); + + return pluginManager; } private void InitializeUmbracoContext() @@ -71,7 +67,7 @@ namespace Umbraco.Tests.PublishedContent { base.TearDown(); - Core.DI.Current.Reset(); + Current.Reset(); } [Test] @@ -184,19 +180,19 @@ namespace Umbraco.Tests.PublishedContent Assert.AreEqual(2, result[1].Id); } - static SolidFacade CreateFacade() + private static SolidFacade CreateFacade() { var caches = new SolidFacade(); var cache = caches.InnerContentCache; var props = new[] - { - new PublishedPropertyType("prop1", 1, "?"), - }; + { + new PublishedPropertyType("prop1", 1, "?"), + }; var contentType1 = new PublishedContentType(1, "ContentType1", Enumerable.Empty(), props); var contentType2 = new PublishedContentType(2, "ContentType2", Enumerable.Empty(), props); - var contentType2s = new PublishedContentType(3, "ContentType2Sub", Enumerable.Empty(), props); + var contentType2Sub = new PublishedContentType(3, "ContentType2Sub", Enumerable.Empty(), props); cache.Add(new SolidPublishedContent(contentType1) { @@ -210,15 +206,15 @@ namespace Umbraco.Tests.PublishedContent ParentId = -1, ChildIds = new int[] {}, Properties = new Collection + { + new SolidPublishedProperty { - new SolidPublishedProperty - { - PropertyTypeAlias = "prop1", - HasValue = true, - Value = 1234, - SourceValue = "1234" - } + PropertyTypeAlias = "prop1", + HasValue = true, + Value = 1234, + SourceValue = "1234" } + } }); cache.Add(new SolidPublishedContent(contentType2) @@ -233,18 +229,18 @@ namespace Umbraco.Tests.PublishedContent ParentId = -1, ChildIds = new int[] { }, Properties = new Collection - { - new SolidPublishedProperty - { - PropertyTypeAlias = "prop1", - HasValue = true, - Value = 1234, - SourceValue = "1234" - } - } + { + new SolidPublishedProperty + { + PropertyTypeAlias = "prop1", + HasValue = true, + Value = 1234, + SourceValue = "1234" + } + } }); - cache.Add(new SolidPublishedContent(contentType2s) + cache.Add(new SolidPublishedContent(contentType2Sub) { Id = 3, SortOrder = 2, @@ -256,15 +252,15 @@ namespace Umbraco.Tests.PublishedContent ParentId = -1, ChildIds = new int[] { }, Properties = new Collection - { - new SolidPublishedProperty - { - PropertyTypeAlias = "prop1", - HasValue = true, - Value = 1234, - SourceValue = "1234" - } - } + { + new SolidPublishedProperty + { + PropertyTypeAlias = "prop1", + HasValue = true, + Value = 1234, + SourceValue = "1234" + } + } }); return caches; diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 53ce8ffdf9..979fa52c1b 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -12,6 +12,7 @@ using Umbraco.Web.PublishedCache; using Umbraco.Core.DI; using Current = Umbraco.Core.DI.Current; using LightInject; +using Umbraco.Tests.Testing; namespace Umbraco.Tests.PublishedContent { @@ -19,9 +20,10 @@ namespace Umbraco.Tests.PublishedContent /// Tests the methods on IPublishedContent using the DefaultPublishedContentStore /// [TestFixture] + [UmbracoTest(PluginManager = UmbracoTestOptions.PluginManager.PerFixture)] public class PublishedContentTests : PublishedContentTestBase { - private PluginManager _pluginManager; + private TypeLoader _typeLoader; public override void SetUp() { @@ -30,14 +32,6 @@ namespace Umbraco.Tests.PublishedContent base.SetUp(); - // this is so the model factory looks into the test assembly - _pluginManager = Current.PluginManager; - Core.DI.Current.PluginManager = new PluginManager(CacheHelper.RuntimeCache, ProfilingLogger, false) - { - AssembliesToScan = _pluginManager.AssembliesToScan - .Union(new[] { typeof(PublishedContentTests).Assembly }) - }; - // need to specify a custom callback for unit tests // AutoPublishedContentTypes generates properties automatically // when they are requested, but we must declare those that we @@ -57,19 +51,23 @@ namespace Umbraco.Tests.PublishedContent ContentTypesCache.GetPublishedContentTypeByAlias = alias => type; } - public override void TearDown() - { - base.TearDown(); - // fixme - wtf, restoring? keeping it accross tests for perfs I guess? - //PluginManager.Current = _pluginManager; - Core.DI.Current.Reset(); - } - protected override void Compose() { base.Compose(); - Container.RegisterSingleton(f => new PublishedContentModelFactory(f.GetInstance().ResolveTypes())); + Container.RegisterSingleton(f => new PublishedContentModelFactory(f.GetInstance().GetTypes())); + } + + protected override TypeLoader CreatePluginManager(IServiceFactory f) + { + var pluginManager = base.CreatePluginManager(f); + + // this is so the model factory looks into the test assembly + pluginManager.AssembliesToScan = pluginManager.AssembliesToScan + .Union(new[] { typeof(PublishedContentTests).Assembly }) + .ToList(); + + return pluginManager; } private readonly Guid _node1173Guid = Guid.NewGuid(); diff --git a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs index 7b416d99c2..00d2772f5e 100644 --- a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs +++ b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs @@ -64,10 +64,10 @@ namespace Umbraco.Tests.Routing // set the default RenderMvcController Current.DefaultRenderMvcControllerType = typeof(RenderMvcController); // fixme WRONG! - var surfaceControllerTypes = new SurfaceControllerTypeCollection(Current.PluginManager.ResolveSurfaceControllers()); + var surfaceControllerTypes = new SurfaceControllerTypeCollection(Current.TypeLoader.ResolveSurfaceControllers()); Container.RegisterInstance(surfaceControllerTypes); - var umbracoApiControllerTypes = new UmbracoApiControllerTypeCollection(Current.PluginManager.ResolveUmbracoApiControllers()); + var umbracoApiControllerTypes = new UmbracoApiControllerTypeCollection(Current.TypeLoader.ResolveUmbracoApiControllers()); Container.RegisterInstance(umbracoApiControllerTypes); Container.RegisterSingleton(_ => new DefaultShortStringHelper(SettingsForTests.GetDefault())); diff --git a/src/Umbraco.Tests/TestHelpers/BaseUsingSqlCeSyntax.cs b/src/Umbraco.Tests/TestHelpers/BaseUsingSqlCeSyntax.cs index fa6d2ef3c9..1d34e0a0d8 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseUsingSqlCeSyntax.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseUsingSqlCeSyntax.cs @@ -40,13 +40,13 @@ namespace Umbraco.Tests.TestHelpers container.RegisterSingleton(factory => Mock.Of()); var logger = new ProfilingLogger(Mock.Of(), Mock.Of()); - var pluginManager = new PluginManager(new NullCacheProvider(), + var pluginManager = new TypeLoader(new NullCacheProvider(), logger, false); container.RegisterInstance(pluginManager); container.RegisterCollectionBuilder() - .Add(() => Current.PluginManager.ResolveAssignedMapperTypes()); + .Add(() => Current.TypeLoader.GetAssignedMapperTypes()); Mappers = container.GetInstance(); var mappers = new NPoco.MapperCollection { new PocoMapper() }; diff --git a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs index bcae539a12..39893edfbc 100644 --- a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs +++ b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs @@ -82,7 +82,7 @@ namespace Umbraco.Tests.TestHelpers Container.GetInstance() .Clear() - .Add(f => f.GetInstance().ResolvePropertyEditors()); + .Add(f => f.GetInstance().GetPropertyEditors()); Container.RegisterSingleton(f => { diff --git a/src/Umbraco.Tests/Testing/UmbracoTestAttribute.cs b/src/Umbraco.Tests/Testing/UmbracoTestAttribute.cs index 89f62b3309..58ce26ee17 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestAttribute.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestAttribute.cs @@ -28,12 +28,6 @@ namespace Umbraco.Tests.Testing public bool AutoMapper { get => _autoMapper.ValueOrDefault(WithApplication); set => _autoMapper.Set(value); } private readonly Settable _autoMapper = new Settable(); - /// - /// Gets or sets a value indicating ... FIXME to be completed - /// - public bool ResetPluginManager { get => _resetPluginManager.ValueOrDefault(false); set => _resetPluginManager.Set(value); } - private readonly Settable _resetPluginManager = new Settable(); - /// /// Gets or sets a value indicating ... FIXME to be completed /// @@ -54,6 +48,13 @@ namespace Umbraco.Tests.Testing public UmbracoTestOptions.Database Database { get => _database.ValueOrDefault(UmbracoTestOptions.Database.None); set => _database.Set(value); } private readonly Settable _database = new Settable(); + /// + /// Gets or sets a value indicating the required plugin manager support. + /// + /// Default is to use the global tests plugin manager. + public UmbracoTestOptions.PluginManager PluginManager { get => _pluginManager.ValueOrDefault(UmbracoTestOptions.PluginManager.Default); set => _pluginManager.Set(value); } + private readonly Settable _pluginManager = new Settable(); + #endregion #region Get @@ -85,10 +86,10 @@ namespace Umbraco.Tests.Testing private UmbracoTestAttribute Merge(UmbracoTestAttribute other) { _autoMapper.Set(other._autoMapper); - _resetPluginManager.Set(other._resetPluginManager); _facadeServiceRepositoryEvents.Set(other._facadeServiceRepositoryEvents); _logger.Set(other._logger); _database.Set(other._database); + _pluginManager.Set(other._pluginManager); return this; } diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index b5b70950ab..556446702e 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -10,6 +10,7 @@ using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Components; using Umbraco.Core.DI; +using Umbraco.Core.DI.CompositionRoots; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -78,7 +79,9 @@ namespace Umbraco.Tests.Testing internal TestObjects TestObjects { get; private set; } - private static PluginManager _pluginManager; + private static TypeLoader _commonTypeLoader; + + private TypeLoader _featureTypeLoader; #region Accessors @@ -125,7 +128,7 @@ namespace Umbraco.Tests.Testing ComposeLogging(Options.Logger); ComposeCacheHelper(); ComposeAutoMapper(Options.AutoMapper); - ComposePluginManager(Options.ResetPluginManager); + ComposePluginManager(Options.PluginManager); ComposeDatabase(Options.Database); ComposeApplication(Options.WithApplication); // etc @@ -186,26 +189,44 @@ namespace Umbraco.Tests.Testing Container.RegisterFrom(); } - protected virtual void ComposePluginManager(bool reset) + protected virtual void ComposePluginManager(UmbracoTestOptions.PluginManager pluginManager) { Container.RegisterSingleton(f => { - if (_pluginManager != null && reset == false) return _pluginManager; - - return _pluginManager = new PluginManager(f.GetInstance().RuntimeCache, f.GetInstance(), false) + switch (pluginManager) { - AssembliesToScan = new[] - { - Assembly.Load("Umbraco.Core"), - Assembly.Load("umbraco"), - Assembly.Load("Umbraco.Tests"), - Assembly.Load("cms"), - Assembly.Load("controls"), - } - }; + case UmbracoTestOptions.PluginManager.Default: + return _commonTypeLoader ?? (_commonTypeLoader = CreateCommonPluginManager(f)); + case UmbracoTestOptions.PluginManager.PerFixture: + return _featureTypeLoader ?? (_featureTypeLoader = CreatePluginManager(f)); + case UmbracoTestOptions.PluginManager.PerTest: + return CreatePluginManager(f); + default: + throw new ArgumentOutOfRangeException(nameof(pluginManager)); + } }); } + protected virtual TypeLoader CreatePluginManager(IServiceFactory f) + { + return CreateCommonPluginManager(f); + } + + private static TypeLoader CreateCommonPluginManager(IServiceFactory f) + { + return new TypeLoader(f.GetInstance().RuntimeCache, f.GetInstance(), false) + { + AssembliesToScan = new[] + { + Assembly.Load("Umbraco.Core"), + Assembly.Load("umbraco"), + Assembly.Load("Umbraco.Tests"), + Assembly.Load("cms"), + Assembly.Load("controls"), + } + }; + } + protected virtual void ComposeDatabase(UmbracoTestOptions.Database option) { if (option == UmbracoTestOptions.Database.None) return; diff --git a/src/Umbraco.Tests/Testing/UmbracoTestOptions.cs b/src/Umbraco.Tests/Testing/UmbracoTestOptions.cs index 67aab30fd5..9dede6ea43 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestOptions.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestOptions.cs @@ -23,5 +23,15 @@ // new database file with schema per test NewSchemaPerTest } + + public enum PluginManager + { + // the default, global plugin manager for tests + Default, + // create one plugin manager for the feature + PerFixture, + // create one plugin manager for each test + PerTest + } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/unit-test-log4net.config b/src/Umbraco.Tests/unit-test-log4net.config index 703471f5a8..25c580bc99 100644 --- a/src/Umbraco.Tests/unit-test-log4net.config +++ b/src/Umbraco.Tests/unit-test-log4net.config @@ -9,7 +9,7 @@ - + diff --git a/src/Umbraco.Web/Current.cs b/src/Umbraco.Web/Current.cs index a187ec6556..4bafad0e81 100644 --- a/src/Umbraco.Web/Current.cs +++ b/src/Umbraco.Web/Current.cs @@ -196,7 +196,7 @@ namespace Umbraco.Web public static IRuntimeState RuntimeState => CoreCurrent.RuntimeState; - public static PluginManager PluginManager => CoreCurrent.PluginManager; + public static TypeLoader TypeLoader => CoreCurrent.TypeLoader; public static UrlSegmentProviderCollection UrlSegmentProviders => CoreCurrent.UrlSegmentProviders; diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index f8b3f78594..2d40daf16e 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -734,7 +734,7 @@ namespace Umbraco.Web.Editors private IEnumerable> GetTreePluginsMetaData() { - var treeTypes = Current.PluginManager.ResolveAttributedTreeControllers(); // fixme inject + var treeTypes = Current.TypeLoader.ResolveAttributedTreeControllers(); // fixme inject //get all plugin trees with their attributes var treesWithAttributes = treeTypes.Select(x => new { diff --git a/src/Umbraco.Web/LightInjectExtensions.cs b/src/Umbraco.Web/LightInjectExtensions.cs index 04ffca0f20..57c3a6f3f3 100644 --- a/src/Umbraco.Web/LightInjectExtensions.cs +++ b/src/Umbraco.Web/LightInjectExtensions.cs @@ -10,38 +10,38 @@ namespace Umbraco.Web internal static class LightInjectExtensions { /// - /// Registers all IControllers using the PluginManager for scanning and caching found instances for the calling assembly + /// Registers all IControllers using the TypeLoader for scanning and caching found instances for the calling assembly /// /// - /// + /// /// - public static void RegisterMvcControllers(this IServiceRegistry container, PluginManager pluginManager, Assembly assembly) + public static void RegisterMvcControllers(this IServiceRegistry container, TypeLoader typeLoader, Assembly assembly) { //TODO: We've already scanned for UmbracoApiControllers and SurfaceControllers - should we scan again // for all controllers? Seems like we should just do this once and then filter. That said here we are // only scanning our own single assembly. Hrm. - container.RegisterControllers(pluginManager, assembly); + container.RegisterControllers(typeLoader, assembly); } /// - /// Registers all IHttpController using the PluginManager for scanning and caching found instances for the calling assembly + /// Registers all IHttpController using the TypeLoader for scanning and caching found instances for the calling assembly /// /// - /// + /// /// - public static void RegisterApiControllers(this IServiceRegistry container, PluginManager pluginManager, Assembly assembly) + public static void RegisterApiControllers(this IServiceRegistry container, TypeLoader typeLoader, Assembly assembly) { //TODO: We've already scanned for UmbracoApiControllers and SurfaceControllers - should we scan again // for all controllers? Seems like we should just do this once and then filter. That said here we are // only scanning our own single assembly. Hrm. - container.RegisterControllers(pluginManager, assembly); + container.RegisterControllers(typeLoader, assembly); } - private static void RegisterControllers(this IServiceRegistry container, PluginManager pluginManager, Assembly assembly) + private static void RegisterControllers(this IServiceRegistry container, TypeLoader typeLoader, Assembly assembly) { - var types = pluginManager.ResolveTypes(specificAssemblies: new[] { assembly }); + var types = typeLoader.GetTypes(specificAssemblies: new[] { assembly }); foreach (var type in types) container.Register(type, new PerRequestLifeTime()); } diff --git a/src/Umbraco.Web/Macros/XsltMacroEngine.cs b/src/Umbraco.Web/Macros/XsltMacroEngine.cs index 539c270354..30d74d47af 100644 --- a/src/Umbraco.Web/Macros/XsltMacroEngine.cs +++ b/src/Umbraco.Web/Macros/XsltMacroEngine.cs @@ -667,7 +667,7 @@ namespace Umbraco.Web.Macros } // get types marked with XsltExtension attribute - var foundExtensions = PluginManager.Current.ResolveXsltExtensions(); + var foundExtensions = TypeLoader.Current.ResolveXsltExtensions(); foreach (var xsltType in foundExtensions) { var attributes = xsltType.GetCustomAttributes(true); diff --git a/src/Umbraco.Web/PluginManagerExtensions.cs b/src/Umbraco.Web/PluginManagerExtensions.cs index 66af41c5ac..d2be8774ec 100644 --- a/src/Umbraco.Web/PluginManagerExtensions.cs +++ b/src/Umbraco.Web/PluginManagerExtensions.cs @@ -24,28 +24,28 @@ namespace Umbraco.Web /// Returns all available IAction in application /// /// - internal static IEnumerable ResolveActions(this PluginManager resolver) + internal static IEnumerable ResolveActions(this TypeLoader resolver) { - return resolver.ResolveTypes(); + return resolver.GetTypes(); } /// /// Returns all available TreeApiController's in application that are attribute with TreeAttribute /// /// /// - internal static IEnumerable ResolveAttributedTreeControllers(this PluginManager resolver) + internal static IEnumerable ResolveAttributedTreeControllers(this TypeLoader resolver) { - return resolver.ResolveTypesWithAttribute(); + return resolver.GetTypesWithAttribute(); } - internal static IEnumerable ResolveSurfaceControllers(this PluginManager resolver) + internal static IEnumerable ResolveSurfaceControllers(this TypeLoader resolver) { - return resolver.ResolveTypes(); + return resolver.GetTypes(); } - internal static IEnumerable ResolveUmbracoApiControllers(this PluginManager resolver) + internal static IEnumerable ResolveUmbracoApiControllers(this TypeLoader resolver) { - return resolver.ResolveTypes(); + return resolver.GetTypes(); } /// @@ -53,9 +53,9 @@ namespace Umbraco.Web /// /// /// - internal static IEnumerable ResolveTrees(this PluginManager resolver) + internal static IEnumerable ResolveTrees(this TypeLoader resolver) { - return resolver.ResolveTypes(); + return resolver.GetTypes(); } @@ -64,9 +64,9 @@ namespace Umbraco.Web /// /// /// - internal static IEnumerable ResolveXsltExtensions(this PluginManager resolver) + internal static IEnumerable ResolveXsltExtensions(this TypeLoader resolver) { - return resolver.ResolveAttributedTypes(); + return resolver.GetAttributedTypes(); } /// @@ -74,9 +74,9 @@ namespace Umbraco.Web /// /// /// - internal static IEnumerable ResolveThumbnailProviders(this PluginManager resolver) + internal static IEnumerable ResolveThumbnailProviders(this TypeLoader resolver) { - return resolver.ResolveTypes(); + return resolver.GetTypes(); } /// @@ -84,9 +84,9 @@ namespace Umbraco.Web /// /// /// - internal static IEnumerable ResolveImageUrlProviders(this PluginManager resolver) + internal static IEnumerable ResolveImageUrlProviders(this TypeLoader resolver) { - return resolver.ResolveTypes(); + return resolver.GetTypes(); } } diff --git a/src/Umbraco.Web/Services/ApplicationTreeService.cs b/src/Umbraco.Web/Services/ApplicationTreeService.cs index 3719c3fb08..09f5c91a1d 100644 --- a/src/Umbraco.Web/Services/ApplicationTreeService.cs +++ b/src/Umbraco.Web/Services/ApplicationTreeService.cs @@ -408,7 +408,7 @@ namespace Umbraco.Web.Services var added = new List(); // Load all Controller Trees by attribute - var types = Current.PluginManager.ResolveTypesWithAttribute(); // fixme inject + var types = Current.TypeLoader.GetTypesWithAttribute(); // fixme inject //convert them to ApplicationTree instances var items = types .Select(x => diff --git a/src/Umbraco.Web/Services/SectionService.cs b/src/Umbraco.Web/Services/SectionService.cs index 0e82a06d52..319858b0a3 100644 --- a/src/Umbraco.Web/Services/SectionService.cs +++ b/src/Umbraco.Web/Services/SectionService.cs @@ -295,7 +295,7 @@ namespace Umbraco.Web.Services // Load all Applications by attribute and add them to the XML config //don't cache the result of this because it is only used once during app startup, caching will just add a bit more mem overhead for no reason - var types = Current.PluginManager.ResolveTypesWithAttribute(cache: false); // fixme - inject + var types = Current.TypeLoader.GetTypesWithAttribute(cache: false); // fixme - inject //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 diff --git a/src/Umbraco.Web/Strategies/Migrations/PostMigrationComponent.cs b/src/Umbraco.Web/Strategies/Migrations/PostMigrationComponent.cs index 352e93fe48..68bb3b1e91 100644 --- a/src/Umbraco.Web/Strategies/Migrations/PostMigrationComponent.cs +++ b/src/Umbraco.Web/Strategies/Migrations/PostMigrationComponent.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.Strategies.Migrations public override void Compose(Composition composition) { composition.Container.RegisterCollectionBuilder() - .Add(factory => factory.GetInstance().ResolveTypes()); + .Add(factory => factory.GetInstance().GetTypes()); } public void Initialize(PostMigrationCollection posts) diff --git a/src/Umbraco.Web/WebRuntimeComponent.cs b/src/Umbraco.Web/WebRuntimeComponent.cs index daa24df458..9400e639c6 100644 --- a/src/Umbraco.Web/WebRuntimeComponent.cs +++ b/src/Umbraco.Web/WebRuntimeComponent.cs @@ -54,7 +54,7 @@ namespace Umbraco.Web composition.Container.RegisterFrom(); - var pluginManager = composition.Container.GetInstance(); + var pluginManager = composition.Container.GetInstance(); var logger = composition.Container.GetInstance(); var proflog = composition.Container.GetInstance(); @@ -105,7 +105,7 @@ namespace Umbraco.Web .AddExtensionObjectProducer(() => pluginManager.ResolveXsltExtensions()); composition.Container.RegisterCollectionBuilder() - .Add(() => pluginManager.ResolveTypes()); + .Add(() => pluginManager.GetTypes()); // set the default RenderMvcController Current.DefaultRenderMvcControllerType = typeof(RenderMvcController); // fixme WRONG! @@ -165,7 +165,7 @@ namespace Umbraco.Web // register *all* checks, except those marked [HideFromTypeFinder] of course composition.Container.RegisterCollectionBuilder() - .Add(() => pluginManager.ResolveTypes()); + .Add(() => pluginManager.GetTypes()); // auto-register views composition.Container.RegisterAuto(typeof(UmbracoViewPage<>)); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/TreeDefinitionCollection.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/TreeDefinitionCollection.cs index 9c019e5d5e..2c4aa17b5a 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/TreeDefinitionCollection.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/TreeDefinitionCollection.cs @@ -140,7 +140,7 @@ namespace umbraco.cms.presentation.Trees if (_ensureTrees == false) { - var foundITrees = Current.PluginManager.ResolveTrees(); + var foundITrees = Current.TypeLoader.ResolveTrees(); var appTrees = Current.Services.ApplicationTreeService.GetAll().ToList(); var apps = Current.Services.SectionService.GetSections().ToList();