From 34518b5ce2e0a528f8e4ea7c510adcf27d791761 Mon Sep 17 00:00:00 2001 From: Chris Payne Date: Thu, 15 Dec 2016 20:54:22 +0000 Subject: [PATCH] Adds IDecorator functionality (#6233) --- .../DefaultShellContainerFactoryTests.cs | 217 +++++++++++++++++- .../ShellBuilders/ShellContainerFactory.cs | 215 +++++++++++++++-- src/Orchard/IDependency.cs | 7 + 3 files changed, 421 insertions(+), 18 deletions(-) diff --git a/src/Orchard.Tests/Environment/ShellBuilders/DefaultShellContainerFactoryTests.cs b/src/Orchard.Tests/Environment/ShellBuilders/DefaultShellContainerFactoryTests.cs index dc75360f2..58386e935 100644 --- a/src/Orchard.Tests/Environment/ShellBuilders/DefaultShellContainerFactoryTests.cs +++ b/src/Orchard.Tests/Environment/ShellBuilders/DefaultShellContainerFactoryTests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using System.Web.Http.Controllers; using System.Web.Mvc; using Autofac; @@ -15,6 +16,7 @@ using Orchard.Environment.Extensions.Models; using Orchard.Environment.ShellBuilders; using Orchard.Environment.Descriptor.Models; using Orchard.Environment.ShellBuilders.Models; +using Orchard.Events; namespace Orchard.Tests.Environment.ShellBuilders { [TestFixture] @@ -347,5 +349,218 @@ namespace Orchard.Tests.Environment.ShellBuilders { Assert.That(proxa, Is.Not.SameAs(proxa2)); Assert.That(setta, Is.Not.SameAs(setta2)); } + + public interface IStubEventHandlerA : IEventHandler { } + public interface IStubEventHandlerB : IEventHandler { } + public class StubEventHandler1 : IStubEventHandlerA { } + public class StubEventHandler2 : IStubEventHandlerA { } + public class StubEventHandler3 : IStubEventHandlerB { } + + [Test] + public void EventHandlersAreNamedAndResolvedCorrectly() { + var settings = CreateSettings(); + var blueprint = CreateBlueprint( + WithDependency(), + WithDependency(), + WithDependency() + ); + + var factory = _container.Resolve(); + var shellContainer = factory.CreateContainer(settings, blueprint); + + var eventHandlers = shellContainer.ResolveNamed>(typeof(IStubEventHandlerA).Name).ToArray(); + + Assert.That(eventHandlers, Is.Not.Null); + Assert.That(eventHandlers.Count(), Is.EqualTo(2)); + Assert.That(eventHandlers[0], Is.InstanceOf()); + Assert.That(eventHandlers[1], Is.InstanceOf()); + } + + public interface ITestDecorator : IDependency { ITestDecorator DecoratedService { get; } } + public class TestDecoratorImpl1 : ITestDecorator { public ITestDecorator DecoratedService => null; } + public class TestDecoratorImpl2 : ITestDecorator { public ITestDecorator DecoratedService => null; } + public class TestDecoratorImpl3 : ITestDecorator { public ITestDecorator DecoratedService => null; } + + public class TestDecorator1 : IDecorator, ITestDecorator { + public TestDecorator1(ITestDecorator decoratedService) { + DecoratedService = decoratedService; + } + + public ITestDecorator DecoratedService { get; } + } + + public class TestDecorator2 : IDecorator, ITestDecorator { + public TestDecorator2(ITestDecorator decoratedService) { + DecoratedService = decoratedService; + } + + public ITestDecorator DecoratedService { get; } + } + + public class TestDecorator3 : IDecorator, ITestDecorator { + public TestDecorator3(ITestDecorator decoratedService) { + DecoratedService = decoratedService; + } + + public ITestDecorator DecoratedService { get; } + } + + [Test] + public void DecoratedComponentsAreResolvedToTheDecorator() { + var settings = CreateSettings(); + var blueprint = CreateBlueprint( + WithDependency(), + WithDependency() + ); + + var factory = _container.Resolve(); + var shellContainer = factory.CreateContainer(settings, blueprint); + + var decorator = shellContainer.Resolve(); + + Assert.That(decorator, Is.Not.Null); + Assert.That(decorator, Is.InstanceOf()); + Assert.That(decorator.DecoratedService, Is.InstanceOf()); + } + + [Test] + public void DecoratedComponentsAreResolvedToTheDecoratorWhenTheDecoratorIsRegisteredFirst() { + var settings = CreateSettings(); + var blueprint = CreateBlueprint( + WithDependency(), + WithDependency() + ); + + var factory = _container.Resolve(); + var shellContainer = factory.CreateContainer(settings, blueprint); + + var decorator = shellContainer.Resolve(); + + Assert.That(decorator, Is.Not.Null); + Assert.That(decorator, Is.InstanceOf()); + Assert.That(decorator.DecoratedService, Is.InstanceOf()); + } + + [Test] + public void DecoratedComponentsAreNeverResolved() { + var settings = CreateSettings(); + var blueprint = CreateBlueprint( + WithDependency(), + WithDependency() + ); + + var factory = _container.Resolve(); + var shellContainer = factory.CreateContainer(settings, blueprint); + + var services = shellContainer.Resolve>(); + + Assert.That(services, Is.Not.Null); + Assert.That(services.Count(), Is.EqualTo(1)); + Assert.That(services.First(), Is.InstanceOf()); + Assert.That(services.First().DecoratedService, Is.InstanceOf()); + } + + [Test] + public void MultipleComponentsCanBeDecoratedWithASingleDecorator() { + var settings = CreateSettings(); + var blueprint = CreateBlueprint( + WithDependency(), + WithDependency(), + WithDependency(), + WithDependency() + ); + + var factory = _container.Resolve(); + var shellContainer = factory.CreateContainer(settings, blueprint); + + var services = shellContainer.Resolve>().ToArray(); + + Assert.That(services, Is.Not.Null); + Assert.That(services.Count(), Is.EqualTo(3)); + + foreach (var service in services) + { + Assert.That(service, Is.InstanceOf()); + } + + Assert.That(services[0].DecoratedService, Is.InstanceOf()); + Assert.That(services[1].DecoratedService, Is.InstanceOf()); + Assert.That(services[2].DecoratedService, Is.InstanceOf()); + } + + [Test] + public void ASingleComponentCanBeDecoratedWithMultipleDecorators() { + var settings = CreateSettings(); + var blueprint = CreateBlueprint( + WithDependency(), + WithDependency(), + WithDependency(), + WithDependency() + ); + + var factory = _container.Resolve(); + var shellContainer = factory.CreateContainer(settings, blueprint); + + var services = shellContainer.Resolve>().ToArray(); + + Assert.That(services, Is.Not.Null); + Assert.That(services.Count(), Is.EqualTo(1)); + + var service = services[0]; + + Assert.That(service, Is.InstanceOf()); + Assert.That(service.DecoratedService, Is.InstanceOf()); + Assert.That(service.DecoratedService.DecoratedService, Is.InstanceOf()); + Assert.That(service.DecoratedService.DecoratedService.DecoratedService, Is.InstanceOf()); + } + + [Test] + public void MultipleComponentsCanBeDecoratedWithMultipleDecorators() { + var settings = CreateSettings(); + var blueprint = CreateBlueprint( + WithDependency(), + WithDependency(), + WithDependency(), + WithDependency(), + WithDependency(), + WithDependency() + ); + + var factory = _container.Resolve(); + var shellContainer = factory.CreateContainer(settings, blueprint); + + var services = shellContainer.Resolve>().ToArray(); + + Assert.That(services, Is.Not.Null); + Assert.That(services.Count(), Is.EqualTo(3)); + + foreach (var service in services) + { + Assert.That(service, Is.InstanceOf()); + Assert.That(service.DecoratedService, Is.InstanceOf()); + Assert.That(service.DecoratedService.DecoratedService, Is.InstanceOf()); + } + + Assert.That(services[0].DecoratedService.DecoratedService.DecoratedService, Is.InstanceOf()); + Assert.That(services[1].DecoratedService.DecoratedService.DecoratedService, Is.InstanceOf()); + Assert.That(services[2].DecoratedService.DecoratedService.DecoratedService, Is.InstanceOf()); + } + + [Test] + public void RegisteringDecoratorsWithoutConcreteThrowsFatalException() { + var settings = CreateSettings(); + var blueprint = CreateBlueprint( + WithDependency(), + WithDependency(), + WithDependency() + ); + + var factory = _container.Resolve(); + + Assert.Throws(delegate { + factory.CreateContainer(settings, blueprint); + }); + } + } } diff --git a/src/Orchard/Environment/ShellBuilders/ShellContainerFactory.cs b/src/Orchard/Environment/ShellBuilders/ShellContainerFactory.cs index 5bcaa4d2c..1c0d79f38 100644 --- a/src/Orchard/Environment/ShellBuilders/ShellContainerFactory.cs +++ b/src/Orchard/Environment/ShellBuilders/ShellContainerFactory.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Web.Hosting; @@ -14,6 +16,8 @@ using Orchard.Environment.AutofacUtil.DynamicProxy2; using Orchard.Environment.Configuration; using Orchard.Environment.ShellBuilders.Models; using Orchard.Events; +using Orchard.Localization; +using Orchard.Logging; namespace Orchard.Environment.ShellBuilders { @@ -28,8 +32,14 @@ namespace Orchard.Environment.ShellBuilders { public ShellContainerFactory(ILifetimeScope lifetimeScope, IShellContainerRegistrations shellContainerRegistrations) { _lifetimeScope = lifetimeScope; _shellContainerRegistrations = shellContainerRegistrations; + + Logger = NullLogger.Instance; + T = NullLocalizer.Instance; } + public ILogger Logger { get; set; } + public Localizer T { get; set; } + public ILifetimeScope CreateContainer(ShellSettings settings, ShellBlueprint blueprint) { var intermediateScope = _lifetimeScope.BeginLifetimeScope( builder => { @@ -56,50 +66,149 @@ namespace Orchard.Environment.ShellBuilders { builder.Register(ctx => blueprint.Descriptor); builder.Register(ctx => blueprint); + var concreteRegistrationNames = new ConcurrentDictionary>(); + var moduleIndex = intermediateScope.Resolve>(); foreach (var item in blueprint.Dependencies.Where(t => typeof(IModule).IsAssignableFrom(t.Type))) { builder.RegisterModule(moduleIndex[item.Type]); } + + var itemsToBeRegistered = new ConcurrentQueue(); + var decorators = new ConcurrentQueue(); foreach (var item in blueprint.Dependencies.Where(t => typeof(IDependency).IsAssignableFrom(t.Type))) { - var registration = RegisterType(builder, item) + // Determine if this service is an IEventHandler + var isEventHandler = typeof (IEventHandler).IsAssignableFrom(item.Type); + + // Harvest any interfaces that this service decorates + var decoratingTypes = item.Type.GetInterfaces() + .Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IDecorator<>)) + .Select(t => t.GetGenericArguments().First()); + + var isDecorator = decoratingTypes != null && decoratingTypes.Any(); + + if (isDecorator && isEventHandler) { + Logger.Error(string.Format("Type `{0}` is an IDecorator, but is also an IEventHandler. Decorating IEventHandlers is not currently supported. This decorator will not be registered.", item.Type.FullName)); + + continue; + } + + if (isDecorator) { + // If this service is a decorator, we need to determine which types it decorates + foreach (var itemToBeRegistered in itemsToBeRegistered) { + foreach (var interfaceType in decoratingTypes) { + if (itemToBeRegistered.InterfaceTypes.Contains(interfaceType)) { + if (itemToBeRegistered.DecoratedTypes == null) { + itemToBeRegistered.DecoratedTypes = new List(); + } + + // Add to the collection of interfaces that are decorated only if this interface type has not previously been added + if (!itemToBeRegistered.DecoratedTypes.Contains(interfaceType)) { + itemToBeRegistered.DecoratedTypes.Add(interfaceType); + } + } + } + } + } + + itemsToBeRegistered.Enqueue(new ItemToBeRegistered {Item = item, InterfaceTypes = GetInterfacesFromBlueprint(item), DecoratingTypes = decoratingTypes, IsEventHandler = isEventHandler}); + } + + foreach (var itemToBeRegistered in itemsToBeRegistered) { + var registration = RegisterType(builder, itemToBeRegistered.Item) + .AsSelf() .EnableDynamicProxy(dynamicProxyContext) .InstancePerLifetimeScope(); - foreach (var interfaceType in item.Type.GetInterfaces() - .Where(itf => typeof(IDependency).IsAssignableFrom(itf) - && !typeof(IEventHandler).IsAssignableFrom(itf))) { - registration = registration.As(interfaceType).AsSelf(); - if (typeof(ISingletonDependency).IsAssignableFrom(interfaceType)) { - registration = registration.InstancePerMatchingLifetimeScope("shell"); + var registrationName = registration.ActivatorData.ImplementationType.FullName; + + registration.Named(registrationName, itemToBeRegistered.Item.Type); + + foreach (var interfaceType in itemToBeRegistered.InterfaceTypes) { + + registration = SetRegistrationScope(interfaceType, registration); + + var itemIsDecorator = itemToBeRegistered.IsDecorator(interfaceType); + var itemIsDecorated = itemToBeRegistered.IsDecorated(interfaceType); + + if (!itemIsDecorated && !itemIsDecorator) { + // This item is not decorated by another implementation of this interface type and is not a decorator. + // It should be registered as the implementation of this interface. The ensures that Autofac will resolve only a single implementation should there be one or more decorators. + registration = registration.As(interfaceType); } - else if (typeof(IUnitOfWorkDependency).IsAssignableFrom(interfaceType)) { - registration = registration.InstancePerMatchingLifetimeScope("work"); + + if (itemIsDecorator) { + // This item decorates the interface currently being registered. + // It needs to be added to the list of decorators so that is can be registered once all of the concrete implementations have been registered. + decorators.Enqueue(new DecoratorRegistration(interfaceType, itemToBeRegistered, itemIsDecorated)); } - else if (typeof(ITransientDependency).IsAssignableFrom(interfaceType)) { - registration = registration.InstancePerDependency(); + else { + // This item is not a decorator. + // We need to add it to the list of concrete implementations. This allows us to know the names of the implementations that need to be decorated should a decorator for this interface exist. + AddConcreteRegistrationName(registrationName, interfaceType, itemToBeRegistered.Item.Type, concreteRegistrationNames); } } - if (typeof(IEventHandler).IsAssignableFrom(item.Type)) { - var interfaces = item.Type.GetInterfaces(); + if (itemToBeRegistered.IsEventHandler) { + var interfaces = itemToBeRegistered.Item.Type.GetInterfaces(); foreach (var interfaceType in interfaces) { // register named instance for each interface, for efficient filtering inside event bus // IEventHandler derived classes only - if (interfaceType.GetInterface(typeof (IEventHandler).Name) != null) { + if (interfaceType.GetInterface(typeof(IEventHandler).Name) != null) { registration = registration.Named(interfaceType.Name); } } } - foreach (var parameter in item.Parameters) { + foreach (var parameter in itemToBeRegistered.Item.Parameters) { registration = registration .WithParameter(parameter.Name, parameter.Value) .WithProperty(parameter.Name, parameter.Value); } } + foreach (var decorator in decorators) { + // We need to ensure that there is an implementation of this service that can be decorated + if (!concreteRegistrationNames.ContainsKey(decorator.InterfaceType) || concreteRegistrationNames[decorator.InterfaceType] == null || !concreteRegistrationNames[decorator.InterfaceType].Any()) { + var exception = new OrchardFatalException(T("The only registered implementations of `{0}` are decorators. In order to avoid circular dependenices, there must be at least one implementation that is not marked with the `OrchardDecorator` attribute.", decorator.InterfaceType.FullName)); + Logger.Fatal(exception, "Could not complete dependency registration as a circular dependency chain has been found."); + + throw exception; + } + + var decoratorNames = new ConcurrentBag(); + + // For every implementation that can be decorated + foreach (var namedRegistration in concreteRegistrationNames[decorator.InterfaceType]) { + var registration = RegisterType(builder, decorator.ItemToBeRegistered.Item) + .AsSelf() + .EnableDynamicProxy(dynamicProxyContext) + .InstancePerLifetimeScope(); + + registration = SetRegistrationScope(decorator.InterfaceType, registration); + + // Create a unique name for the decorator + var decoratorName = string.Format("{0}-{1}", namedRegistration.Name, decorator.ItemToBeRegistered.Item.Type.FullName); + registration = registration.Named(decoratorName, decorator.ItemToBeRegistered.Item.Type); + + // Tell Autofac to resolve the decorated service with the implementation that has already been registered + registration = registration.WithParameter( + (p, c) => p.ParameterType == decorator.InterfaceType, + (p, c) => c.ResolveNamed(namedRegistration.Name, namedRegistration.ImplementationType)); + + if (!decorator.IsDecorated) { + // This is the last decorator in the stack, so register it as the implmentation of the interface that it is decorating + registration = registration.As(decorator.InterfaceType); + } + + decoratorNames.Add(new NamedRegistration(decoratorName, decorator.ItemToBeRegistered.Item.Type)); + } + + // Update the collection of implmentation names that can be decorated to contain only the decorators (this allows us to stack decorators) + concreteRegistrationNames[decorator.InterfaceType] = decoratorNames; + } + foreach (var item in blueprint.Controllers) { var serviceKeyName = (item.AreaName + "/" + item.ControllerName).ToLowerInvariant(); var serviceKeyType = item.Type; @@ -108,8 +217,7 @@ namespace Orchard.Environment.ShellBuilders { .Keyed(serviceKeyName) .Keyed(serviceKeyType) .WithMetadata("ControllerType", item.Type) - .InstancePerDependency() - ; + .InstancePerDependency(); } foreach (var item in blueprint.HttpControllers) { @@ -147,5 +255,78 @@ namespace Orchard.Environment.ShellBuilders { .WithProperty("Feature", item.Feature) .WithMetadata("Feature", item.Feature); } + + private IEnumerable GetInterfacesFromBlueprint(DependencyBlueprint blueprint) { + return blueprint.Type.GetInterfaces() + .Where(itf => typeof(IDependency).IsAssignableFrom(itf) + && !typeof(IEventHandler).IsAssignableFrom(itf)); + } + + private IRegistrationBuilder SetRegistrationScope(Type interfaceType, IRegistrationBuilder decoratorRegistration) { + if (typeof (ISingletonDependency).IsAssignableFrom(interfaceType)) { + decoratorRegistration = decoratorRegistration.InstancePerMatchingLifetimeScope("shell"); + } + else if (typeof (IUnitOfWorkDependency).IsAssignableFrom(interfaceType)) { + decoratorRegistration = decoratorRegistration.InstancePerMatchingLifetimeScope("work"); + } + else if (typeof (ITransientDependency).IsAssignableFrom(interfaceType)) { + decoratorRegistration = decoratorRegistration.InstancePerDependency(); + } + return decoratorRegistration; + } + + private void AddConcreteRegistrationName(string registrationName, Type interfaceType, Type implementationType, ConcurrentDictionary> concreteRegistrationNames) { + if (concreteRegistrationNames.ContainsKey(interfaceType) + && concreteRegistrationNames[interfaceType] != null + && !concreteRegistrationNames[interfaceType].Any(nr=>nr.Name==registrationName)) { + concreteRegistrationNames[interfaceType].Add(new NamedRegistration(registrationName, implementationType)); + } + else { + concreteRegistrationNames[interfaceType] = new ConcurrentBag {new NamedRegistration(registrationName, implementationType)}; + } + } + + private class ItemToBeRegistered { + public DependencyBlueprint Item { get; set; } + public IEnumerable InterfaceTypes { get; set; } + /// + /// The types that this item decorates + /// + public IEnumerable DecoratingTypes { get; set; } + /// + /// The types that this item implements that are decorated by another item + /// + public IList DecoratedTypes { get; set; } + public bool IsEventHandler { get; set; } + + public bool IsDecorated(Type interfaceType) { + return DecoratedTypes != null && DecoratedTypes.Contains(interfaceType); + } + public bool IsDecorator(Type interfaceType) { + return DecoratingTypes != null && DecoratingTypes.Contains(interfaceType); + } + } + + private class NamedRegistration { + public NamedRegistration(string name, Type implementationType) { + Name = name; + ImplementationType = implementationType; + } + + public string Name { get; } + public Type ImplementationType { get; } + } + + private class DecoratorRegistration { + public DecoratorRegistration(Type interfaceType, ItemToBeRegistered itemToBeRegistered, bool isDecorated) { + InterfaceType = interfaceType; + ItemToBeRegistered = itemToBeRegistered; + IsDecorated = isDecorated; + } + + public Type InterfaceType { get; } + public ItemToBeRegistered ItemToBeRegistered { get; } + public bool IsDecorated { get; } + } } } \ No newline at end of file diff --git a/src/Orchard/IDependency.cs b/src/Orchard/IDependency.cs index e5417ff0a..176744065 100644 --- a/src/Orchard/IDependency.cs +++ b/src/Orchard/IDependency.cs @@ -27,6 +27,13 @@ namespace Orchard { public interface ITransientDependency : IDependency { } + /// + /// Indicates that a service should be registered as a decorator. + /// + /// The type that this service decorates. Must be , and this service must also implement this type. + public interface IDecorator where T : IDependency { + } + public abstract class Component : IDependency { protected Component() {