diff --git a/src/Umbraco.Core/Components/BootLoader.cs b/src/Umbraco.Core/Components/BootLoader.cs index 696ae20064..6f600c402e 100644 --- a/src/Umbraco.Core/Components/BootLoader.cs +++ b/src/Umbraco.Core/Components/BootLoader.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.Components private IUmbracoComponent[] _components; private bool _booted; - private const int LogThresholdMilliseconds = 200; + private const int LogThresholdMilliseconds = 100; /// /// Initializes a new instance of the class. @@ -30,37 +30,6 @@ namespace Umbraco.Core.Components _proflog = container.GetInstance(); } - // fixme - sort out it all - // fixme - what about executing when no database? when not configured? see all event handler! - // rules - // - // UmbracoCoreComponent is special and requires every IUmbracoCoreComponent - // IUmbracoUserComponent is special and requires UmbracoCoreComponent - // - // process Enable/Disable for *all* regardless of whether they'll end up being enabled or disabled - // process Require *only* for those that end up being enabled - // requiring something that's disabled is going to cause an exception - // - // works: - // gets the list of all discovered components - // handles dependencies and order via topological graph - // handle enable/disable (precedence?) - // OR vice-versa as, if it's disabled, it has no dependency! - // BUT then the order is pretty much random - bah - // for each component, run Compose - // for each component, discover & run Initialize methods - // - // should we register components on a clone? (benchmark!) - // should we get then in a scope => disposed? - // do we want to keep them around? - // + - // what's with ServiceProvider and PluginManager? - // - // do we need events? - // initialize, starting, started - // become - // ?, compose, initialize - private class EnableInfo { public bool Enabled; @@ -74,6 +43,7 @@ namespace Umbraco.Core.Components using (_proflog.TraceDuration($"Booting Umbraco {UmbracoVersion.GetSemanticVersion().ToSemanticString()} on {NetworkHelper.MachineName}.", "Booted.")) { var orderedComponentTypes = PrepareComponentTypes(componentTypes); + InstanciateComponents(orderedComponentTypes); ComposeComponents(); InitializeComponents(); @@ -95,19 +65,20 @@ namespace Umbraco.Core.Components { var componentTypeList = componentTypes.ToList(); + // cannot remove that one - ever if (componentTypeList.Contains(typeof(UmbracoCoreComponent)) == false) componentTypeList.Add(typeof(UmbracoCoreComponent)); var enabled = new Dictionary(); // process the enable/disable attributes + // these two attributes are *not* inherited and apply to *classes* only (not interfaces). // remote declarations (when a component enables/disables *another* component) // have priority over local declarations (when a component disables itself) so that - // ppl can enable components that, by default, are disabled + // ppl can enable components that, by default, are disabled. // what happens in case of conflicting remote declarations is unspecified. more // precisely, the last declaration to be processed wins, but the order of the - // declarations depends on the type finder and is unspecified - // we *could* fix this by adding a weight property to both attributes... + // declarations depends on the type finder and is unspecified. foreach (var componentType in componentTypeList) { foreach (var attr in componentType.GetCustomAttributes()) @@ -146,29 +117,52 @@ namespace Umbraco.Core.Components { temp.Clear(); + //// for tests + //Console.WriteLine("Components & Dependencies:"); + //Console.WriteLine(type.FullName); + //foreach (var attribute in type.GetCustomAttributes()) + // Console.WriteLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue ? (attribute.Weak.Value ? " (weak)" : " (strong)") : "")); + //foreach (var i in type.GetInterfaces()) + //{ + // Console.WriteLine(" " + i.FullName); + // foreach (var attribute in i.GetCustomAttributes()) + // Console.WriteLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue ? (attribute.Weak.Value ? " (weak)" : " (strong)") : "")); + //} + //Console.WriteLine("/"); + //Console.WriteLine(); + // get attributes - // these attributes are *not* inherited because we want to custom-inherit for interfaces only + // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only var attributes = type .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) .Concat(type.GetCustomAttributes()); - // requiring an interface => require any enabled component implementing that interface - // requiring a class => require only that class + // what happens in case of conflicting attributes (different strong/weak for same type) is not specified. foreach (var attr in attributes) { + // requiring an interface = require any enabled component implementing that interface + // unless strong, and then require at least one enabled component implementing that interface if (attr.RequiredType.IsInterface) - temp.AddRange(componentTypeList.Where(x => attr.RequiredType.IsAssignableFrom(x))); + { + var implems = componentTypeList.Where(x => attr.RequiredType.IsAssignableFrom(x)).ToList(); + if (implems.Count > 0) + temp.AddRange(implems); + else if (attr.Weak == false) // if explicitely set to !weak, is strong, else is weak + throw new Exception($"Broken component dependency: {type.FullName} -> {attr.RequiredType.FullName}."); + } + // requiring a class = require that the component is enabled + // unless weak, and then requires it if it is enabled else - temp.Add(attr.RequiredType); + { + if (componentTypeList.Contains(attr.RequiredType)) + temp.Add(attr.RequiredType); + else if (attr.Weak != true) // if not explicitely set to weak, is strong + throw new Exception($"Broken component dependency: {type.FullName} -> {attr.RequiredType.FullName}."); + } } - var dependsOn = temp.Distinct().ToArray(); - - // check for broken dependencies - foreach (var broken in temp.Where(x => componentTypeList.Contains(x) == false)) - throw new Exception($"Broken component dependency: {type.FullName} -> {broken.FullName}."); - - items.Add(new TopologicalSorter.DependencyField(type.FullName, dependsOn.Select(x => x.FullName).ToArray(), new Lazy(() => type))); + var dependsOn = temp.Distinct().Select(x => x.FullName).ToArray(); + items.Add(new TopologicalSorter.DependencyField(type.FullName, dependsOn, new Lazy(() => type))); } return TopologicalSorter.GetSortedItems(items); } diff --git a/src/Umbraco.Core/Components/DisableComponentAttribute.cs b/src/Umbraco.Core/Components/DisableComponentAttribute.cs index d80681aeb7..f7ff71e119 100644 --- a/src/Umbraco.Core/Components/DisableComponentAttribute.cs +++ b/src/Umbraco.Core/Components/DisableComponentAttribute.cs @@ -2,17 +2,37 @@ namespace Umbraco.Core.Components { + /// + /// Indicates that a component should be disabled. + /// + /// + /// If a type is specified, disables the component of that type, else disables the component marked with the attribute. + /// This attribute is *not* inherited. + /// This attribute applies to classes only, it is not possible to enable/disable interfaces. + /// If a component ends up being both enabled and disabled: attributes marking the component itself have lower priority + /// than attributes on *other* components, eg if a component declares itself as disabled it is possible to enable it from + /// another component. Anything else is unspecified. + /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] public class DisableComponentAttribute : Attribute { + /// + /// Initializes a new instance of the class. + /// public DisableComponentAttribute() { } + /// + /// Initializes a new instance of the class. + /// public DisableComponentAttribute(Type disabledType) { DisabledType = disabledType; } + /// + /// Gets the disabled type, or null if it is the component marked with the attribute. + /// public Type DisabledType { get; } } } diff --git a/src/Umbraco.Core/Components/EnableComponentAttribute.cs b/src/Umbraco.Core/Components/EnableComponentAttribute.cs index 81cb6666b5..fa76dc2404 100644 --- a/src/Umbraco.Core/Components/EnableComponentAttribute.cs +++ b/src/Umbraco.Core/Components/EnableComponentAttribute.cs @@ -2,17 +2,37 @@ namespace Umbraco.Core.Components { + /// + /// Indicates that a component should be enabled. + /// + /// + /// If a type is specified, enables the component of that type, else enables the component marked with the attribute. + /// This attribute is *not* inherited. + /// This attribute applies to classes only, it is not possible to enable/disable interfaces. + /// If a component ends up being both enabled and disabled: attributes marking the component itself have lower priority + /// than attributes on *other* components, eg if a component declares itself as disabled it is possible to enable it from + /// another component. Anything else is unspecified. + /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] public class EnableComponentAttribute : Attribute { + /// + /// Initializes a new instance of the class. + /// public EnableComponentAttribute() { } + /// + /// Initializes a new instance of the class. + /// public EnableComponentAttribute(Type enabledType) { EnabledType = enabledType; } + /// + /// Gets the enabled type, or null if it is the component marked with the attribute. + /// public Type EnabledType { get; } } } diff --git a/src/Umbraco.Core/Components/IRuntimeComponent.cs b/src/Umbraco.Core/Components/IRuntimeComponent.cs new file mode 100644 index 0000000000..7f89d21e87 --- /dev/null +++ b/src/Umbraco.Core/Components/IRuntimeComponent.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Core.Components +{ + public interface IRuntimeComponent : IUmbracoComponent + { } +} diff --git a/src/Umbraco.Core/Components/IUmbracoCoreComponent.cs b/src/Umbraco.Core/Components/IUmbracoCoreComponent.cs index 459da50b5e..28ff286da3 100644 --- a/src/Umbraco.Core/Components/IUmbracoCoreComponent.cs +++ b/src/Umbraco.Core/Components/IUmbracoCoreComponent.cs @@ -1,5 +1,6 @@ namespace Umbraco.Core.Components { + [RequireComponent(typeof(IRuntimeComponent))] public interface IUmbracoCoreComponent : IUmbracoComponent { } } diff --git a/src/Umbraco.Core/Components/RequireComponentAttribute.cs b/src/Umbraco.Core/Components/RequireComponentAttribute.cs index 614e0b008a..e9e85ca344 100644 --- a/src/Umbraco.Core/Components/RequireComponentAttribute.cs +++ b/src/Umbraco.Core/Components/RequireComponentAttribute.cs @@ -2,14 +2,58 @@ namespace Umbraco.Core.Components { - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = true)] + /// + /// Indicates that a component requires another component. + /// + /// + /// This attribute is *not* inherited. This means that a component class inheriting from + /// another component class does *not* inherit its requirements. However, the bootloader checks + /// the *interfaces* of every component for their requirements, so requirements declared on + /// interfaces are inherited by every component class implementing the interface. + /// When targetting a class, indicates a dependency on the component which must be enabled, + /// unless the requirement has explicitely been declared as weak (and then, only if the component + /// is enabled). + /// When targetting an interface, indicates a dependency on enabled components implementing + /// the interface. It could be no component at all, unless the requirement has explicitely been + /// declared as strong (and at least one component must be enabled). + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] public class RequireComponentAttribute : Attribute { + /// + /// Initializes a new instance of the class. + /// + /// The type of the required component. public RequireComponentAttribute(Type requiredType) { + if (typeof(IUmbracoComponent).IsAssignableFrom(requiredType) == false) + throw new ArgumentException($"Type {requiredType.FullName} is invalid here because it does not implement {typeof(IUmbracoComponent).FullName}."); RequiredType = requiredType; } + /// + /// Initializes a new instance of the class. + /// + /// The type of the required component. + /// A value indicating whether the requirement is weak. + public RequireComponentAttribute(Type requiredType, bool weak) + : this(requiredType) + { + Weak = weak; + } + + /// + /// Gets the required type. + /// public Type RequiredType { get; } + + /// + /// Gets a value indicating whether the requirement is weak. + /// + /// Returns true if the requirement is weak (requires the other component if it + /// is enabled), false if the requirement is strong (requires the other component to be + /// enabled), and null if unspecified, in which case it is strong for classes and weak for + /// interfaces. + public bool? Weak { get; } } } diff --git a/src/Umbraco.Core/CoreRuntime.cs b/src/Umbraco.Core/CoreRuntime.cs index ebc0fc5396..e176f3cc1c 100644 --- a/src/Umbraco.Core/CoreRuntime.cs +++ b/src/Umbraco.Core/CoreRuntime.cs @@ -100,9 +100,6 @@ namespace Umbraco.Core // then compose Compose(container); - // fixme! - Compose1(container); - // the boot loader boots using a container scope, so anything that is PerScope will // be disposed after the boot loader has booted, and anything else will remain. // note that this REQUIRES that perWebRequestScope has NOT been enabled yet, else @@ -130,86 +127,6 @@ namespace Umbraco.Core container.RegisterInstance(cache.RuntimeCache); container.RegisterInstance(new PluginManager(cache.RuntimeCache, ProfilingLogger)); - - // register from roots // fixme - components?! - container.RegisterFrom(); // fixme - used to be before caches? - container.RegisterFrom(); - container.RegisterFrom(); - container.RegisterFrom(); - } - - protected virtual void Compose1(ServiceContainer container) - { - ServiceProvider = new ActivatorServiceProvider(); - - //create the plugin manager - //TODO: this is currently a singleton but it would be better if it weren't. Unfortunately the only way to get - // rid of this singleton would be to put it into IoC and then use the ServiceLocator pattern. - PluginManager.Current = PluginManager = Current.PluginManager; //new PluginManager(ApplicationCache.RuntimeCache, ProfilingLogger, true); - - //TODO: Don't think we'll need this when the resolvers are all container resolvers - container.RegisterSingleton(); - container.RegisterSingleton(); - container.Register(factory => FileSystemProviderManager.Current.GetFileSystemProvider()); - - // fixme - should we capture Logger, etc here or use factory? - - // register manifest builder, will be injected in eg PropertyEditorCollectionBuilder - container.RegisterSingleton(factory - => new ManifestParser(factory.GetInstance(), new DirectoryInfo(IOHelper.MapPath("~/App_Plugins")), factory.GetInstance())); - container.RegisterSingleton(); - - PropertyEditorCollectionBuilder.Register(container) - .AddProducer(factory => factory.GetInstance().ResolvePropertyEditors()); - - ParameterEditorCollectionBuilder.Register(container) - .AddProducer(factory => factory.GetInstance().ResolveParameterEditors()); - - // register our predefined validators - ValidatorCollectionBuilder.Register(container) - .Add() - .Add() - .Add() - .Add() - .Add() - .Add(); - - // register a server registrar, by default it's the db registrar unless the dev - // has the legacy dist calls enabled - fixme - should obsolete the legacy thing - container.RegisterSingleton(factory => UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled - ? (IServerRegistrar)new ConfigServerRegistrar(UmbracoConfig.For.UmbracoSettings()) - : (IServerRegistrar)new DatabaseServerRegistrar( - new Lazy(() => factory.GetInstance().Services.ServerRegistrationService), - new DatabaseServerRegistrarOptions())); - - // by default we'll use the database server messenger with default options (no callbacks), - // this will be overridden in the web startup - // fixme - painful, have to take care of lifetime! - we CANNOT ask users to remember! - // fixme - same issue with PublishedContentModelFactory and many more, I guess! - container.RegisterSingleton(factory - => new DatabaseServerMessenger(factory.GetInstance(), true, new DatabaseServerMessengerOptions())); - - CacheRefresherCollectionBuilder.Register(container) - .AddProducer(factory => factory.GetInstance().ResolveCacheRefreshers()); - - PackageActionCollectionBuilder.Register(container) - .AddProducer(f => f.GetInstance().ResolvePackageActions()); - - MigrationCollectionBuilder.Register(container) - .AddProducer(factory => factory.GetInstance().ResolveTypes()); - - // need to filter out the ones we dont want!! fixme - what does that mean? - PropertyValueConverterCollectionBuilder.Register(container) - .Append(factory => factory.GetInstance().ResolveTypes()); - - container.RegisterSingleton(factory - => new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault(factory.GetInstance()))); - - UrlSegmentProviderCollectionBuilder.Register(container) - .Append(); - - // by default, register a noop factory - container.RegisterSingleton(); } /// @@ -225,9 +142,6 @@ namespace Umbraco.Core protected ProfilingLogger ProfilingLogger { get; private set; } - // fixme do we need that one? - protected PluginManager PluginManager { get; private set; } - #endregion #region Getters @@ -270,20 +184,6 @@ namespace Umbraco.Core throw new UmbracoStartupFailedException("Umbraco cannot start: a connection string is configured but Umbraco could not connect to the database."); } - protected void InitializeModelMappers() - { - Mapper.Initialize(configuration => - { - // fixme why ApplicationEventHandler?! - //foreach (var m in ApplicationEventsResolver.Current.ApplicationEventHandlers.OfType()) - foreach (var m in Container.GetAllInstances()) - { - Logger.Debug("FIXME " + m.GetType().FullName); - m.ConfigureMappings(configuration, Current.ApplicationContext); - } - }); - } - /// /// Special method to extend the use of Umbraco by enabling the consumer to overwrite /// the absolute path to the root of an Umbraco site/solution, which is used for stuff @@ -302,8 +202,6 @@ namespace Umbraco.Core protected ServiceContainer Container => Current.Container; // fixme kill - protected IServiceProvider ServiceProvider { get; private set; } - public virtual IRuntime Initialize() { if (_isInitialized) @@ -316,15 +214,6 @@ namespace Umbraco.Core "Umbraco application startup complete"); - // register - //Compose1(Container); - - //TODO: Remove these for v8! - LegacyPropertyEditorIdToAliasConverter.CreateMappingsForCoreEditors(); - LegacyParameterEditorAliasConverter.CreateMappingsForCoreEditors(); - - InitializeModelMappers(); - //now we need to call the initialize methods //Create a 'child'container which is a copy of all of the current registrations and begin a sub scope for it // this child container will be used to manage the application event handler instances and the scope will be @@ -414,7 +303,7 @@ namespace Umbraco.Core if (_isComplete) throw new InvalidOperationException("The boot manager has already been completed"); - FreezeResolution(); + Complete2(); //Here we need to make sure the db can be connected to EnsureDatabaseConnection(); @@ -469,8 +358,7 @@ namespace Umbraco.Core return this; } - protected virtual void FreezeResolution() - { - } + protected virtual void Complete2() + { } } } diff --git a/src/Umbraco.Core/CoreRuntimeComponent.cs b/src/Umbraco.Core/CoreRuntimeComponent.cs new file mode 100644 index 0000000000..b000d4a5bb --- /dev/null +++ b/src/Umbraco.Core/CoreRuntimeComponent.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AutoMapper; +using LightInject; +using Umbraco.Core.Cache; +using Umbraco.Core.Components; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.DependencyInjection; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Manifest; +using Umbraco.Core.Models.Mapping; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Persistence.Migrations; +using Umbraco.Core.Plugins; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.Core.Sync; +using Umbraco.Core._Legacy.PackageActions; + +namespace Umbraco.Core +{ + public class CoreRuntimeComponent : UmbracoComponentBase, IRuntimeComponent + { + public override void Compose(ServiceContainer container) + { + base.Compose(container); + + // register from roots + container.RegisterFrom(); + container.RegisterFrom(); + container.RegisterFrom(); + container.RegisterFrom(); + + //TODO: Don't think we'll need this when the resolvers are all container resolvers + container.RegisterSingleton(); + container.RegisterSingleton(); + container.Register(factory => FileSystemProviderManager.Current.GetFileSystemProvider()); + + // fixme - should we capture Logger, etc here or use factory? + + // register manifest builder, will be injected in eg PropertyEditorCollectionBuilder + container.RegisterSingleton(factory + => new ManifestParser(factory.GetInstance(), new DirectoryInfo(IOHelper.MapPath("~/App_Plugins")), factory.GetInstance())); + container.RegisterSingleton(); + + PropertyEditorCollectionBuilder.Register(container) + .AddProducer(factory => factory.GetInstance().ResolvePropertyEditors()); + + ParameterEditorCollectionBuilder.Register(container) + .AddProducer(factory => factory.GetInstance().ResolveParameterEditors()); + + // register our predefined validators + ValidatorCollectionBuilder.Register(container) + .Add() + .Add() + .Add() + .Add() + .Add() + .Add(); + + // register a server registrar, by default it's the db registrar unless the dev + // has the legacy dist calls enabled - fixme - should obsolete the legacy thing + container.RegisterSingleton(factory => UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled + ? (IServerRegistrar)new ConfigServerRegistrar(UmbracoConfig.For.UmbracoSettings()) + : (IServerRegistrar)new DatabaseServerRegistrar( + new Lazy(() => factory.GetInstance().Services.ServerRegistrationService), + new DatabaseServerRegistrarOptions())); + + // by default we'll use the database server messenger with default options (no callbacks), + // this will be overridden in the web startup + // fixme - painful, have to take care of lifetime! - we CANNOT ask users to remember! + // fixme - same issue with PublishedContentModelFactory and many more, I guess! + container.RegisterSingleton(factory + => new DatabaseServerMessenger(factory.GetInstance(), true, new DatabaseServerMessengerOptions())); + + CacheRefresherCollectionBuilder.Register(container) + .AddProducer(factory => factory.GetInstance().ResolveCacheRefreshers()); + + PackageActionCollectionBuilder.Register(container) + .AddProducer(f => f.GetInstance().ResolvePackageActions()); + + MigrationCollectionBuilder.Register(container) + .AddProducer(factory => factory.GetInstance().ResolveTypes()); + + // need to filter out the ones we dont want!! fixme - what does that mean? + PropertyValueConverterCollectionBuilder.Register(container) + .Append(factory => factory.GetInstance().ResolveTypes()); + + container.RegisterSingleton(factory + => new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault(factory.GetInstance()))); + + UrlSegmentProviderCollectionBuilder.Register(container) + .Append(); + + // by default, register a noop factory + container.RegisterSingleton(); + } + + public void Initialize(IEnumerable modelMapperConfigurations) + { + //TODO: Remove these for v8! + LegacyPropertyEditorIdToAliasConverter.CreateMappingsForCoreEditors(); + LegacyParameterEditorAliasConverter.CreateMappingsForCoreEditors(); + + InitializeModelMappers(modelMapperConfigurations); + + } + + private void InitializeModelMappers(IEnumerable modelMapperConfigurations) + { + Mapper.Initialize(configuration => + { + // fixme why ApplicationEventHandler?! + //foreach (var m in ApplicationEventsResolver.Current.ApplicationEventHandlers.OfType()) + foreach (var m in modelMapperConfigurations) + { + //Logger.Debug("FIXME " + m.GetType().FullName); + m.ConfigureMappings(configuration, Current.ApplicationContext); + } + }); + } + } +} diff --git a/src/Umbraco.Core/DependencyInjection/Current.cs b/src/Umbraco.Core/DependencyInjection/Current.cs index 861156200e..5b9e033a50 100644 --- a/src/Umbraco.Core/DependencyInjection/Current.cs +++ b/src/Umbraco.Core/DependencyInjection/Current.cs @@ -68,8 +68,17 @@ namespace Umbraco.Core.DependencyInjection set { _applicationContext = value; } } + // 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 - => Container.GetInstance(); + { + get { return _pluginManager ?? (_pluginManager = Container.TryGetInstance() ?? PluginManager.Default); } + set { _pluginManager = value; } + } public static UrlSegmentProviderCollection UrlSegmentProviders => Container.GetInstance(); diff --git a/src/Umbraco.Core/DependencyInjection/LightInjectExtensions.cs b/src/Umbraco.Core/DependencyInjection/LightInjectExtensions.cs index c5fb643e27..72b06e1918 100644 --- a/src/Umbraco.Core/DependencyInjection/LightInjectExtensions.cs +++ b/src/Umbraco.Core/DependencyInjection/LightInjectExtensions.cs @@ -27,6 +27,9 @@ namespace Umbraco.Core.DependencyInjection // dependencies for properties that is annotated with the InjectAttribute." container.EnableAnnotatedPropertyInjection(); + // see notes in MixedScopeManagerProvider + container.ScopeManagerProvider = new MixedScopeManagerProvider(); + // self-register container.Register(_ => container); diff --git a/src/Umbraco.Core/DependencyInjection/MixedScopeManagerProvider.cs b/src/Umbraco.Core/DependencyInjection/MixedScopeManagerProvider.cs new file mode 100644 index 0000000000..c5321286b1 --- /dev/null +++ b/src/Umbraco.Core/DependencyInjection/MixedScopeManagerProvider.cs @@ -0,0 +1,41 @@ +using LightInject; +using LightInject.Web; + +namespace Umbraco.Core.DependencyInjection +{ + // by default, the container's scope manager provider is PerThreadScopeManagerProvider, + // and then container.EnablePerWebRequestScope() replaces it with PerWebRequestScopeManagerProvider, + // however if any delate has been compiled already at that point, it captures the scope + // manager provider and changing it afterwards has no effect for that delegate. + // + // therefore, Umbraco uses the mixed scope manager provider, which initially wraps an instance + // of PerThreadScopeManagerProvider and then can replace that wrapped instance with an instance + // of PerWebRequestScopeManagerProvider - but all delegates see is the mixed one - and therefore + // they can transition without issues. + // + // the mixed provider is installed in container.ConfigureUmbracoCore() and then, + // when doing eg container.EnableMvc() or anything that does container.EnablePerWebRequestScope() + // we need to take great care to preserve the mixed scope manager provider! + + public class MixedScopeManagerProvider : IScopeManagerProvider + { + private IScopeManagerProvider _provider; + + public MixedScopeManagerProvider() + { + _provider = new PerThreadScopeManagerProvider(); + } + + public void EnablePerWebRequestScope() + { + if (_provider is PerWebRequestScopeManagerProvider) return; + _provider = new PerWebRequestScopeManagerProvider(); + } + + public ScopeManager GetScopeManager() + { + return _provider.GetScopeManager(); + } + } +} + diff --git a/src/Umbraco.Core/Plugins/PluginManager.cs b/src/Umbraco.Core/Plugins/PluginManager.cs index 606589c6dc..0185991869 100644 --- a/src/Umbraco.Core/Plugins/PluginManager.cs +++ b/src/Umbraco.Core/Plugins/PluginManager.cs @@ -29,10 +29,7 @@ namespace Umbraco.Core.Plugins public class PluginManager { private const string CacheKey = "umbraco-plugins.list"; - private static object _instanceLocker = new object(); private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); - private static PluginManager _instance; - private static bool _hasInstance; private readonly IRuntimeCacheProvider _runtimeCache; private readonly ProfilingLogger _logger; @@ -96,42 +93,17 @@ namespace Umbraco.Core.Plugins } } - // fixme - somehow we NEED to get rid of this Current accessor + public static PluginManager Current => DependencyInjection.Current.PluginManager; - /// - /// Gets the current plugin manager. - /// - /// - /// Ensures that no matter what, only one plugin manager is created, and thus proper caching always takes place. - /// The setter is generally only used for unit tests + when creating the master plugin manager in CoreBootManager. - /// - public static PluginManager Current + internal static PluginManager Default { get { - return LazyInitializer.EnsureInitialized(ref _instance, ref _hasInstance, ref _instanceLocker, () => - { - var appctx = ApplicationContext.Current; - var cacheProvider = appctx == null // fixme - should Current have an ApplicationCache? - ? new NullCacheProvider() - : appctx.ApplicationCache.RuntimeCache; - ProfilingLogger profilingLogger; - if (appctx == null) - { - // fixme - should Current have a ProfilingLogger? - profilingLogger = new ProfilingLogger(DependencyInjection.Current.Logger, DependencyInjection.Current.Profiler); - } - else - { - profilingLogger = appctx.ProfilingLogger; - } - return new PluginManager(cacheProvider, profilingLogger); - }); - } - internal set - { - _hasInstance = true; - _instance = value; + var appctx = ApplicationContext.Current; + var cacheProvider = appctx == null // fixme - should Current have an ApplicationCache? + ? new NullCacheProvider() + : appctx.ApplicationCache.RuntimeCache; + return new PluginManager(cacheProvider, DependencyInjection.Current.ProfilingLogger); } } @@ -449,112 +421,116 @@ namespace Umbraco.Core.Plugins #region Resolve Types - private IEnumerable ResolveTypes( - Func> finder, - TypeResolutionKind resolutionType, - bool cacheResult) - { - using (var readLock = new UpgradeableReadLock(Locker)) - { - var typesFound = new List(); + private bool _reportedChange; + private IEnumerable ResolveTypes(Func> finder, TypeResolutionKind resolutionType, bool cacheResult) + { + using (var rlock = new UpgradeableReadLock(Locker)) + { using (_logger.DebugDuration( - $"Starting resolution types of {typeof(T).FullName}", - $"Completed resolution of types of {typeof(T).FullName}", // cannot contain typesFound.Count as it's evaluated before the find! + $"Resolving {typeof(T).FullName}", + $"Resolved {typeof(T).FullName}", // cannot contain typesFound.Count as it's evaluated before the find! 50)) { - //check if the TypeList already exists, if so return it, if not we'll create it - var typeList = _types.SingleOrDefault(x => x.IsTypeList(resolutionType)); + // resolve within a lock & timer + return ResolveTypes2(finder, resolutionType, cacheResult, rlock); + } + } - //need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505 - if (cacheResult && typeList != null) + } + + private IEnumerable ResolveTypes2(Func> finder, TypeResolutionKind resolutionType, bool cacheResult, UpgradeableReadLock rlock) + { + // check if the TypeList already exists, if so return it, if not we'll create it + var typeList = _types.SingleOrDefault(x => x.IsTypeList(resolutionType)); + + //need to put some logging here to try to figure out why this is happening: http://issues.umbraco.org/issue/U4-3505 + if (cacheResult && typeList != null) + { + _logger.Logger.Debug($"Resolving {typeof(T).FullName} ({resolutionType}): found a cached type list."); + } + + //if we're not caching the result then proceed, or if the type list doesn't exist then proceed + if (cacheResult == false || typeList == null) + { + // upgrade to a write lock since we're adding to the collection + rlock.UpgradeToWriteLock(); + + typeList = new TypeList(resolutionType); + + 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("Existing typeList found for {0} with resolution type {1}", () => typeof(T), () => resolutionType); + _logger.Logger.Debug("Assemblies changes detected, need to rescan everything."); + _reportedChange = true; } - - //if we're not caching the result then proceed, or if the type list doesn't exist then proceed - if (cacheResult == false || typeList == null) - { - //upgrade to a write lock since we're adding to the collection - readLock.UpgradeToWriteLock(); - - typeList = new TypeList(resolutionType); - - //we first need to look into our cache file (this has nothing to do with the 'cacheResult' parameter which caches in memory). - //if assemblies have not changed and the cache file actually exists, then proceed to try to lookup by the cache file. - if (RequiresRescanning == false && File.Exists(GetPluginListFilePath())) - { - var fileCacheResult = TryGetCachedPluginsFromFile(resolutionType); - - //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 (fileCacheResult.Exception is CachedPluginNotFoundInFileException) - { - _logger.Logger.Debug("Tried to find typelist for type {0} and resolution {1} in file cache but the type was not found so loading types by assembly scan ", () => typeof(T), () => resolutionType); - - //we don't have a cache for this so proceed to look them up by scanning - LoadViaScanningAndUpdateCacheFile(typeList, resolutionType, finder); - } - else - { - if (fileCacheResult.Success) - { - var successfullyLoadedFromCache = true; - //we have a previous cache for this so we don't need to scan we just load what has been found in the file - foreach (var t in fileCacheResult.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 then we have problems with that. - var type = BuildManager.GetType(t, true); - typeList.AddType(type); - } - catch (Exception ex) - { - //if there are any exceptions loading types, we have to exist, this should never happen so - //we will need to revert to scanning for types. - successfullyLoadedFromCache = false; - _logger.Logger.Error("Could not load a cached plugin type: " + t + " now reverting to re-scanning assemblies for the base type: " + typeof(T).FullName, ex); - break; - } - } - if (successfullyLoadedFromCache == false) - { - //we need to manually load by scanning if loading from the file was not successful. - LoadViaScanningAndUpdateCacheFile(typeList, resolutionType, finder); - } - else - { - _logger.Logger.Debug("Loaded plugin types {0} with resolution {1} from persisted cache", () => typeof(T), () => resolutionType); - } - } - } - } - else - { - _logger.Logger.Debug("Assembly changes detected, loading types {0} for resolution {1} by assembly scan", () => typeof(T), () => resolutionType); - - //we don't have a cache for this so proceed to look them up by scanning - LoadViaScanningAndUpdateCacheFile(typeList, resolutionType, finder); - } - - //only add the cache if we are to cache the results - if (cacheResult) - { - //add the type list to the collection - var added = _types.Add(typeList); - - _logger.Logger.Debug("Caching of typelist for type {0} and resolution {1} was successful = {2}", () => typeof(T), () => resolutionType, () => added); - - } - } - typesFound = typeList.GetTypes().ToList(); } - return typesFound; + if (scan == false) + { + // if we don't have to scan, try the cache file + var fileCacheResult = TryGetCachedPluginsFromFile(resolutionType); + + // 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 (fileCacheResult.Exception is CachedPluginNotFoundInFileException || fileCacheResult.Success == false) + { + _logger.Logger.Debug($"Resolving {typeof(T).FullName} ({resolutionType}): failed to load from cache file, must scan assemblies."); + scan = true; + } + else + { + // successfully retrieved types from the file cache: load + foreach (var type in fileCacheResult.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 + typeList.AddType(BuildManager.GetType(type, true)); + } + catch (Exception ex) + { + // in case of any exception, we have to exit, and revert to scanning + _logger.Logger.Error($"Resolving {typeof(T).FullName} ({resolutionType}): failed to load cache file type {type}, reverting to scanning assemblies.", ex); + scan = true; + break; + } + } + if (scan == false) + { + _logger.Logger.Debug($"Resolving {typeof(T).FullName} ({resolutionType}): loaded types from cache file."); + } + } + } + + if (scan) + { + // either we had to scan, or we could not resolve the types from the cache file - scan now + _logger.Logger.Debug($"Resolving {typeof(T).FullName} ({resolutionType}): scanning assemblies."); + LoadViaScanningAndUpdateCacheFile(typeList, resolutionType, finder); + } + + if (scan && cacheResult) + { + // if we are to cache the results, add the list to the collection + var added = _types.Add(typeList); + _logger.Logger.Debug($"Resolved {typeof(T).FullName} ({resolutionType}), caching (added = {added.ToString().ToLowerInvariant()})."); + } + else + { + _logger.Logger.Debug($"Resolved {typeof(T).FullName} ({resolutionType})."); + } } + + return typeList.GetTypes().ToList(); } /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index fccf2442cd..546146d7a8 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -37,6 +37,9 @@ false + + C:\Users\Stéphane\.nuget\packages\LightInject.Web\1.0.0.7\lib\net45\LightInject.Web.dll + @@ -112,6 +115,7 @@ + @@ -218,6 +222,7 @@ + @@ -231,6 +236,7 @@ + diff --git a/src/Umbraco.Tests/Components/ComponentTests.cs b/src/Umbraco.Tests/Components/ComponentTests.cs index d4c2bdfb52..1cfc8993e5 100644 --- a/src/Umbraco.Tests/Components/ComponentTests.cs +++ b/src/Umbraco.Tests/Components/ComponentTests.cs @@ -136,6 +136,59 @@ namespace Umbraco.Tests.Components Assert.AreEqual(typeof(Component9), Composed[2]); } + [Test] + public void WeakDependencies() + { + var container = new ServiceContainer(); + container.ConfigureUmbracoCore(); + + var logger = Mock.Of(); + var profiler = new LogProfiler(logger); + container.RegisterInstance(logger); + container.RegisterInstance(profiler); + container.RegisterInstance(new ProfilingLogger(logger, profiler)); + + var thing = new BootLoader(container); + Composed.Clear(); + thing.Boot(new[] { typeof(Component10) }); + Assert.AreEqual(1, Composed.Count); + Assert.AreEqual(typeof(Component10), Composed[0]); + + thing = new BootLoader(container); + Composed.Clear(); + Assert.Throws(() => thing.Boot(new[] { typeof(Component11) })); + + thing = new BootLoader(container); + Composed.Clear(); + Assert.Throws(() => thing.Boot(new[] { typeof(Component2) })); + + thing = new BootLoader(container); + Composed.Clear(); + thing.Boot(new[] { typeof(Component12) }); + Assert.AreEqual(1, Composed.Count); + Assert.AreEqual(typeof(Component12), Composed[0]); + } + + [Test] + public void DisableMissing() + { + var container = new ServiceContainer(); + container.ConfigureUmbracoCore(); + + var logger = Mock.Of(); + var profiler = new LogProfiler(logger); + container.RegisterInstance(logger); + container.RegisterInstance(profiler); + container.RegisterInstance(new ProfilingLogger(logger, profiler)); + + var thing = new BootLoader(container); + Composed.Clear(); + thing.Boot(new[] { typeof(Component6), typeof(Component8) }); // 8 disables 7 which is not in the list + Assert.AreEqual(2, Composed.Count); + Assert.AreEqual(typeof(Component6), Composed[0]); + Assert.AreEqual(typeof(Component8), Composed[1]); + } + public class TestComponentBase : UmbracoComponentBase { public override void Compose(ServiceContainer container) @@ -184,6 +237,18 @@ namespace Umbraco.Tests.Components public class Component9 : TestComponentBase, ITestComponent { } + [RequireComponent(typeof(ITestComponent))] + public class Component10 : TestComponentBase + { } + + [RequireComponent(typeof(ITestComponent), false)] + public class Component11 : TestComponentBase + { } + + [RequireComponent(typeof(Component4), true)] + public class Component12 : TestComponentBase, IUmbracoCoreComponent + { } + public interface ISomeResource { } public class SomeResource : ISomeResource { } diff --git a/src/Umbraco.Tests/Migrations/Upgrades/BaseUpgradeTest.cs b/src/Umbraco.Tests/Migrations/Upgrades/BaseUpgradeTest.cs index d2035d498d..1e94649322 100644 --- a/src/Umbraco.Tests/Migrations/Upgrades/BaseUpgradeTest.cs +++ b/src/Umbraco.Tests/Migrations/Upgrades/BaseUpgradeTest.cs @@ -5,6 +5,7 @@ using NPoco; using NUnit.Framework; using Semver; using Umbraco.Core; +using Umbraco.Core.DependencyInjection; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Migrations; @@ -96,7 +97,7 @@ namespace Umbraco.Tests.Migrations.Upgrades [TearDown] public virtual void TearDown() { - PluginManager.Current = null; + Current.PluginManager = null; TestHelper.CleanContentDirectories(); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs index 792ad788fd..678f11dca2 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs @@ -33,7 +33,7 @@ namespace Umbraco.Tests.PublishedContent // this is so the model factory looks into the test assembly _pluginManager = PluginManager.Current; - PluginManager.Current = new PluginManager(new NullCacheProvider(), ProfilingLogger, false) + Core.DependencyInjection.Current.PluginManager = new PluginManager(new NullCacheProvider(), ProfilingLogger, false) { AssembliesToScan = _pluginManager.AssembliesToScan .Union(new[] { typeof (PublishedContentMoreTests).Assembly}) @@ -76,7 +76,6 @@ namespace Umbraco.Tests.PublishedContent { base.TearDown(); - PluginManager.Current = _pluginManager; Core.DependencyInjection.Current.Reset(); } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 39d59ac1b4..0073eb691b 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -30,7 +30,7 @@ namespace Umbraco.Tests.PublishedContent // this is so the model factory looks into the test assembly _pluginManager = PluginManager.Current; - PluginManager.Current = new PluginManager(CacheHelper.RuntimeCache, ProfilingLogger, false) + Core.DependencyInjection.Current.PluginManager = new PluginManager(CacheHelper.RuntimeCache, ProfilingLogger, false) { AssembliesToScan = _pluginManager.AssembliesToScan .Union(new[] { typeof(PublishedContentTests).Assembly }) @@ -58,7 +58,8 @@ namespace Umbraco.Tests.PublishedContent public override void TearDown() { base.TearDown(); - PluginManager.Current = _pluginManager; + // fixme - wtf, restoring? keeping it accross tests for perfs I guess? + //PluginManager.Current = _pluginManager; Core.DependencyInjection.Current.Reset(); } diff --git a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs index fd7bd8beba..55cb5a330b 100644 --- a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs +++ b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs @@ -64,17 +64,6 @@ namespace Umbraco.Tests.Runtimes base.Boot(container); } - protected override void Compose1(ServiceContainer container) - { - base.Compose1(container); - - container.Register(factory => SettingsForTests.GetDefault()); - container.Register(factory => new DatabaseContext( - factory.GetInstance(), - factory.GetInstance()), new PerContainerLifetime()); - container.RegisterSingleton(); - } - protected override IEnumerable GetComponentTypes() { return new[] { typeof(TestComponent) }; @@ -99,6 +88,13 @@ namespace Umbraco.Tests.Runtimes public override void Compose(ServiceContainer container) { base.Compose(container); + + container.Register(factory => SettingsForTests.GetDefault()); + container.Register(factory => new DatabaseContext( + factory.GetInstance(), + factory.GetInstance()), new PerContainerLifetime()); + container.RegisterSingleton(); + Composed = true; } diff --git a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs index 2c28b31821..ec7c682d35 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs @@ -189,7 +189,7 @@ namespace Umbraco.Tests.TestHelpers { if (PluginManagerResetRequired) { - PluginManager.Current = null; + Core.DependencyInjection.Current.PluginManager = null; } } @@ -246,9 +246,10 @@ namespace Umbraco.Tests.TestHelpers /// protected virtual void SetupPluginManager() { - if (PluginManager.Current == null || PluginManagerResetRequired) + // fixme - oops + if (/*PluginManager.Current == null ||*/ PluginManagerResetRequired) { - PluginManager.Current = new PluginManager(CacheHelper.RuntimeCache, ProfilingLogger, false) + Core.DependencyInjection.Current.PluginManager = new PluginManager(CacheHelper.RuntimeCache, ProfilingLogger, false) { AssembliesToScan = new[] { @@ -266,8 +267,7 @@ namespace Umbraco.Tests.TestHelpers /// Inheritors can override this to setup any resolvers before resolution is frozen /// protected virtual void FreezeResolution() - { - } + { } protected ApplicationContext ApplicationContext => ApplicationContext.Current; diff --git a/src/Umbraco.Tests/TestHelpers/BaseUsingSqlCeSyntax.cs b/src/Umbraco.Tests/TestHelpers/BaseUsingSqlCeSyntax.cs index 5dd646ec07..060427fb16 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseUsingSqlCeSyntax.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseUsingSqlCeSyntax.cs @@ -38,7 +38,7 @@ namespace Umbraco.Tests.TestHelpers container.RegisterSingleton(factory => Mock.Of()); var logger = new ProfilingLogger(Mock.Of(), Mock.Of()); - var pluginManager = PluginManager.Current = new PluginManager(new NullCacheProvider(), + var pluginManager = new PluginManager(new NullCacheProvider(), logger, false); container.RegisterInstance(pluginManager); @@ -61,7 +61,6 @@ namespace Umbraco.Tests.TestHelpers public virtual void TearDown() { //MappingResolver.Reset(); - PluginManager.Current = null; Current.Reset(); } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0ecb89c00c..d56393086d 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -296,6 +296,7 @@ ASPXCodeBehind + diff --git a/src/Umbraco.Web/WebRuntime.cs b/src/Umbraco.Web/WebRuntime.cs index 660860ac9e..a19e826d5d 100644 --- a/src/Umbraco.Web/WebRuntime.cs +++ b/src/Umbraco.Web/WebRuntime.cs @@ -72,18 +72,10 @@ namespace Umbraco.Web { base.Boot(container); - // that one must come *last* because there is no "request scope" during boot, - // and components are initialized within a scope so that anything they create - // that is per-scope is disposed one they have initialized. - container.EnablePerWebRequestScope(); - - // fixme - see compose + if it's here then components cannot change anything? - // should we use collections? eg MvcControllerCollection and ApiControllerCollection? - var pluginManager = container.GetInstance(); - container.EnableMvc(); // that one does enable PerWebRequest scope - container.RegisterMvcControllers(pluginManager, GetType().Assembly); - container.EnableWebApi(GlobalConfiguration.Configuration); - container.RegisterApiControllers(pluginManager, GetType().Assembly); + // now is the time to switch over to perWebRequest scopes + var smp = container.ScopeManagerProvider as MixedScopeManagerProvider; + if (smp == null) throw new Exception("Container.ScopeManagerProvider is not MixedScopeManagerProvider."); + smp.EnablePerWebRequestScope(); } /// @@ -96,202 +88,6 @@ namespace Umbraco.Web public override void Compose(ServiceContainer container) { base.Compose(container); - - // fixme - var pluginManager = container.GetInstance(); - - // fixme - suspecting one of these to enable PerWebRequest scope - //container.EnableMvc(); // yes, that one! - //container.RegisterMvcControllers(pluginManager, GetType().Assembly); - //container.EnableWebApi(GlobalConfiguration.Configuration); - //container.RegisterApiControllers(pluginManager, GetType().Assembly); - } - - protected override void Compose1(ServiceContainer container) - { - // register model mappers - container.RegisterFrom(); - - base.Compose1(container); - - // support web request scope - // note: everything that is PerRequestLifeTime will be disposed by LightInject at the end of the request - // fixme - temp - moved to... find it - //container.EnablePerWebRequestScope(); - - // register the http context and umbraco context accessors - // we *should* use the HttpContextUmbracoContextAccessor, however there are cases when - // we have no http context, eg when booting Umbraco or in background threads, so instead - // let's use an hybrid accessor that can fall back to a ThreadStatic context. - container.RegisterSingleton(); // replaces HttpContext.Current - container.RegisterSingleton(); - - // register a per-request HttpContextBase object - // is per-request so only one wrapper is created per request - container.Register(factory => new HttpContextWrapper(factory.GetInstance().HttpContext), new PerRequestLifeTime()); - - // register the facade accessor - the "current" facade is in the umbraco context - container.RegisterSingleton(); - - // register the umbraco database accessor - // have to use the hybrid thing... - container.RegisterSingleton(); - - // register a per-request UmbracoContext object - // no real need to be per request but assuming it is faster - container.Register(factory => factory.GetInstance().UmbracoContext, new PerRequestLifeTime()); - - // register the umbraco helper - container.RegisterSingleton(); - - // replace some services - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - container.RegisterSingleton(); - - container.RegisterSingleton(); - - // fixme moved too wtf - //// IoC setup for LightInject for MVC/WebApi - //container.EnableMvc(); - //container.RegisterMvcControllers(PluginManager, GetType().Assembly); - //container.EnableWebApi(GlobalConfiguration.Configuration); - //container.RegisterApiControllers(PluginManager, GetType().Assembly); - - XsltExtensionCollectionBuilder.Register(container) - .AddExtensionObjectProducer(() => PluginManager.ResolveXsltExtensions()); - - EditorValidatorCollectionBuilder.Register(container) - .AddProducer(() => PluginManager.ResolveTypes()); - - // set the default RenderMvcController - Current.DefaultRenderMvcControllerType = typeof(RenderMvcController); // fixme WRONG! - - //Override the default server messenger, we need to check if the legacy dist calls is enabled, if that is the - // case, then we'll set the default messenger to be the old one, otherwise we'll set it to the db messenger - // which will always be on. - if (UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled) - { - //set the legacy one by default - this maintains backwards compat - container.Register(factory => new BatchedWebServiceServerMessenger(() => - { - var applicationContext = factory.GetInstance(); - //we should not proceed to change this if the app/database is not configured since there will - // be no user, plus we don't need to have server messages sent if this is the case. - if (applicationContext.IsConfigured && applicationContext.DatabaseContext.IsDatabaseConfigured) - { - //disable if they are not enabled - if (UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled == false) - { - return null; - } - - try - { - var user = applicationContext.Services.UserService.GetUserById(UmbracoConfig.For.UmbracoSettings().DistributedCall.UserId); - return new Tuple(user.Username, user.RawPasswordValue); - } - catch (Exception e) - { - ProfilingLogger.Logger.Error("An error occurred trying to set the IServerMessenger during application startup", e); - return null; - } - } - ProfilingLogger.Logger.Warn("Could not initialize the DefaultServerMessenger, the application is not configured or the database is not configured"); - return null; - }), new PerContainerLifetime()); - } - else - { - container.Register(factory => new BatchedDatabaseServerMessenger( - factory.GetInstance(), - true, - //Default options for web including the required callbacks to build caches - new DatabaseServerMessengerOptions - { - //These callbacks will be executed if the server has not been synced - // (i.e. it is a new server or the lastsynced.txt file has been removed) - InitializingCallbacks = new Action[] - { - //rebuild the xml cache file if the server is not synced - () => - { - // rebuild the facade caches entirely, if the server is not synced - // this is equivalent to DistributedCache RefreshAllFacade but local only - // (we really should have a way to reuse RefreshAllFacade... locally) - // note: refresh all content & media caches does refresh content types too - var svc = Current.FacadeService; - bool ignored1, ignored2; - svc.Notify(new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }); - svc.Notify(new[] { new ContentCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out ignored1, out ignored2); - svc.Notify(new[] { new MediaCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out ignored1); - }, - //rebuild indexes if the server is not synced - // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific - // indexes then they can adjust this logic themselves. - () => RebuildIndexes(false) - } - }), new PerContainerLifetime()); - } - - ActionCollectionBuilder.Register(container) - .SetProducer(() => PluginManager.ResolveActions()); - - var surfaceControllerTypes = new SurfaceControllerTypeCollection(PluginManager.ResolveSurfaceControllers()); - container.RegisterInstance(surfaceControllerTypes); - - var umbracoApiControllerTypes = new UmbracoApiControllerTypeCollection(PluginManager.ResolveUmbracoApiControllers()); - container.RegisterInstance(umbracoApiControllerTypes); - - // both TinyMceValueConverter (in Core) and RteMacroRenderingValueConverter (in Web) will be - // discovered when CoreBootManager configures the converters. We HAVE to remove one of them - // here because there cannot be two converters for one property editor - and we want the full - // RteMacroRenderingValueConverter that converts macros, etc. So remove TinyMceValueConverter. - // (the limited one, defined in Core, is there for tests) - same for others - container.GetInstance() - .Remove() - .Remove() - .Remove() - .Remove(); - - // add all known factories, devs can then modify this list on application - // startup either by binding to events or in their own global.asax - FilteredControllerFactoryCollectionBuilder.Register(container) - .Append(); - - UrlProviderCollectionBuilder.Register(container) - //.Append() // not enabled by default - .Append() - .Append(); - - container.Register(); - - ContentFinderCollectionBuilder.Register(container) - // all built-in finders in the correct order, - // devs can then modify this list on application startup - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append(); - - container.Register(); - - ThumbnailProviderCollectionBuilder.Register(container) - .Add(PluginManager.ResolveThumbnailProviders()); - - ImageUrlProviderCollectionBuilder.Register(container) - .Append(PluginManager.ResolveImageUrlProviders()); - - container.RegisterSingleton(); - - HealthCheckCollectionBuilder.Register(container) - .AddProducer(() => PluginManager.ResolveTypes()) - .Exclude(); // fixme must remove else NuCache dies! - // but we should also have one for NuCache AND NuCache should be a component that does all this } #region Getters @@ -476,9 +272,9 @@ namespace Umbraco.Web /// Override this method in order to ensure that the UmbracoContext is also created, this can only be /// created after resolution is frozen! /// - protected override void FreezeResolution() + protected override void Complete2() { - base.FreezeResolution(); + base.Complete2(); //before we do anything, we'll ensure the umbraco context //see: http://issues.umbraco.org/issue/U4-1717 diff --git a/src/Umbraco.Web/WebRuntimeComponent.cs b/src/Umbraco.Web/WebRuntimeComponent.cs new file mode 100644 index 0000000000..fd4473237c --- /dev/null +++ b/src/Umbraco.Web/WebRuntimeComponent.cs @@ -0,0 +1,250 @@ +using System; +using System.Web; +using System.Web.Http; +using Examine; +using LightInject; +using Umbraco.Core; +using Umbraco.Core.Components; +using Umbraco.Core.Configuration; +using Umbraco.Core.DependencyInjection; +using Umbraco.Core.Dictionary; +using Umbraco.Core.Events; +using Umbraco.Core.Logging; +using Umbraco.Core.Macros; +using Umbraco.Core.Persistence; +using Umbraco.Core.Plugins; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Changes; +using Umbraco.Core.Sync; +using Umbraco.Web.Cache; +using Umbraco.Web.DependencyInjection; +using Umbraco.Web.Dictionary; +using Umbraco.Web.Editors; +using Umbraco.Web.HealthCheck; +using Umbraco.Web.HealthCheck.Checks.DataIntegrity; +using Umbraco.Web.Media; +using Umbraco.Web.Media.ThumbnailProviders; +using Umbraco.Web.Mvc; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.Routing; +using Umbraco.Web.Services; +using Umbraco.Web.WebApi; +using Umbraco.Web._Legacy.Actions; +using UmbracoExamine; +using Action = System.Action; + +namespace Umbraco.Web +{ + [RequireComponent(typeof(CoreRuntimeComponent))] + public class WebRuntimeComponent : UmbracoComponentBase, IRuntimeComponent + { + public override void Compose(ServiceContainer container) + { + base.Compose(container); + + container.RegisterFrom(); + + var pluginManager = container.GetInstance(); + var logger = container.GetInstance(); + + // register the http context and umbraco context accessors + // we *should* use the HttpContextUmbracoContextAccessor, however there are cases when + // we have no http context, eg when booting Umbraco or in background threads, so instead + // let's use an hybrid accessor that can fall back to a ThreadStatic context. + container.RegisterSingleton(); // replaces HttpContext.Current + container.RegisterSingleton(); + + // register a per-request HttpContextBase object + // is per-request so only one wrapper is created per request + container.Register(factory => new HttpContextWrapper(factory.GetInstance().HttpContext), new PerRequestLifeTime()); + + // register the facade accessor - the "current" facade is in the umbraco context + container.RegisterSingleton(); + + // register the umbraco database accessor + // have to use the hybrid thing... + container.RegisterSingleton(); + + // register a per-request UmbracoContext object + // no real need to be per request but assuming it is faster + container.Register(factory => factory.GetInstance().UmbracoContext, new PerRequestLifeTime()); + + // register the umbraco helper + container.RegisterSingleton(); + + // replace some services + container.RegisterSingleton(); + container.RegisterSingleton(); + container.RegisterSingleton(); + container.RegisterSingleton(); + + container.RegisterSingleton(); + + // fixme - still, doing it before we get the INITIALIZE scope is a BAD idea + // fixme - also note that whatever you REGISTER in a scope, stays registered => create the scope beforehand? + // fixme - BUT not enough, once per-request is enabled it WANTS per-request scope even though a scope already exists + // IoC setup for LightInject for MVC/WebApi + // see comments on MixedScopeManagerProvider for explainations of what we are doing here + var smp = container.ScopeManagerProvider as MixedScopeManagerProvider; + if (smp == null) throw new Exception("Container.ScopeManagerProvider is not MixedScopeManagerProvider."); + container.EnableMvc(); // does container.EnablePerWebRequestScope() + container.ScopeManagerProvider = smp; // reverts - we will do it last (in WebRuntime) + + container.RegisterMvcControllers(pluginManager, GetType().Assembly); + container.EnableWebApi(GlobalConfiguration.Configuration); + container.RegisterApiControllers(pluginManager, GetType().Assembly); + + XsltExtensionCollectionBuilder.Register(container) + .AddExtensionObjectProducer(() => pluginManager.ResolveXsltExtensions()); + + EditorValidatorCollectionBuilder.Register(container) + .AddProducer(() => pluginManager.ResolveTypes()); + + // set the default RenderMvcController + Current.DefaultRenderMvcControllerType = typeof(RenderMvcController); // fixme WRONG! + + //Override the default server messenger, we need to check if the legacy dist calls is enabled, if that is the + // case, then we'll set the default messenger to be the old one, otherwise we'll set it to the db messenger + // which will always be on. + if (UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled) + { + //set the legacy one by default - this maintains backwards compat + container.Register(factory => new BatchedWebServiceServerMessenger(() => + { + var applicationContext = factory.GetInstance(); + //we should not proceed to change this if the app/database is not configured since there will + // be no user, plus we don't need to have server messages sent if this is the case. + if (applicationContext.IsConfigured && applicationContext.DatabaseContext.IsDatabaseConfigured) + { + //disable if they are not enabled + if (UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled == false) + { + return null; + } + + try + { + var user = applicationContext.Services.UserService.GetUserById(UmbracoConfig.For.UmbracoSettings().DistributedCall.UserId); + return new Tuple(user.Username, user.RawPasswordValue); + } + catch (Exception e) + { + logger.Error("An error occurred trying to set the IServerMessenger during application startup", e); + return null; + } + } + logger.Warn("Could not initialize the DefaultServerMessenger, the application is not configured or the database is not configured"); + return null; + }), new PerContainerLifetime()); + } + else + { + container.Register(factory => new BatchedDatabaseServerMessenger( + factory.GetInstance(), + true, + //Default options for web including the required callbacks to build caches + new DatabaseServerMessengerOptions + { + //These callbacks will be executed if the server has not been synced + // (i.e. it is a new server or the lastsynced.txt file has been removed) + InitializingCallbacks = new Action[] + { + //rebuild the xml cache file if the server is not synced + () => + { + // rebuild the facade caches entirely, if the server is not synced + // this is equivalent to DistributedCache RefreshAllFacade but local only + // (we really should have a way to reuse RefreshAllFacade... locally) + // note: refresh all content & media caches does refresh content types too + var svc = Current.FacadeService; + bool ignored1, ignored2; + svc.Notify(new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }); + svc.Notify(new[] { new ContentCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out ignored1, out ignored2); + svc.Notify(new[] { new MediaCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out ignored1); + }, + //rebuild indexes if the server is not synced + // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific + // indexes then they can adjust this logic themselves. + () => RebuildIndexes(false) + } + }), new PerContainerLifetime()); + } + + ActionCollectionBuilder.Register(container) + .SetProducer(() => pluginManager.ResolveActions()); + + var surfaceControllerTypes = new SurfaceControllerTypeCollection(pluginManager.ResolveSurfaceControllers()); + container.RegisterInstance(surfaceControllerTypes); + + var umbracoApiControllerTypes = new UmbracoApiControllerTypeCollection(pluginManager.ResolveUmbracoApiControllers()); + container.RegisterInstance(umbracoApiControllerTypes); + + // both TinyMceValueConverter (in Core) and RteMacroRenderingValueConverter (in Web) will be + // discovered when CoreBootManager configures the converters. We HAVE to remove one of them + // here because there cannot be two converters for one property editor - and we want the full + // RteMacroRenderingValueConverter that converts macros, etc. So remove TinyMceValueConverter. + // (the limited one, defined in Core, is there for tests) - same for others + container.GetInstance() + .Remove() + .Remove() + .Remove() + .Remove(); + + // add all known factories, devs can then modify this list on application + // startup either by binding to events or in their own global.asax + FilteredControllerFactoryCollectionBuilder.Register(container) + .Append(); + + UrlProviderCollectionBuilder.Register(container) + //.Append() // not enabled by default + .Append() + .Append(); + + container.Register(); + + ContentFinderCollectionBuilder.Register(container) + // all built-in finders in the correct order, + // devs can then modify this list on application startup + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); + + container.Register(); + + ThumbnailProviderCollectionBuilder.Register(container) + .Add(pluginManager.ResolveThumbnailProviders()); + + ImageUrlProviderCollectionBuilder.Register(container) + .Append(pluginManager.ResolveImageUrlProviders()); + + container.RegisterSingleton(); + + HealthCheckCollectionBuilder.Register(container) + .AddProducer(() => pluginManager.ResolveTypes()) + .Exclude(); // fixme must remove else NuCache dies! + // but we should also have one for NuCache AND NuCache should be a component that does all this + } + + protected virtual void RebuildIndexes(bool onlyEmptyIndexes) + { + if (Current.ApplicationContext.IsConfigured == false || Current.ApplicationContext.DatabaseContext.IsDatabaseConfigured == false) + { + return; + } + + foreach (var indexer in ExamineManager.Instance.IndexProviders) + { + if (onlyEmptyIndexes == false || indexer.Value.IsIndexNew()) + { + indexer.Value.RebuildIndex(); + } + } + } + } +}