diff --git a/LightweightIocContainer/Exceptions/CircularDependencyException.cs b/LightweightIocContainer/Exceptions/CircularDependencyException.cs new file mode 100644 index 0000000..213d977 --- /dev/null +++ b/LightweightIocContainer/Exceptions/CircularDependencyException.cs @@ -0,0 +1,63 @@ +// Author: Gockner, Simon +// Created: 2019-11-05 +// Copyright(c) 2019 SimonG. All Rights Reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using LightweightIocContainer.Interfaces; + +namespace LightweightIocContainer.Exceptions +{ + /// + /// A circular dependency was detected during + /// + internal class CircularDependencyException : Exception + { + /// + /// A circular dependency was detected during + /// + /// The currently resolving + /// The resolve stack at the time the was thrown + public CircularDependencyException(Type resolvingType, List resolveStack) + { + ResolvingType = resolvingType; + ResolveStack = resolveStack; + } + + + /// + /// The currently resolving + /// + public Type ResolvingType { get; } + + /// + /// The resolve stack at the time the was thrown + /// + public List ResolveStack { get; } + + /// + /// The exception message + /// + public override string Message + { + get + { + string message = $"Circular dependency has been detected when trying to resolve `{ResolvingType}`.\n" + + "Resolve stack that resulted in the circular dependency:\n" + + $"\t`{ResolvingType}` resolved as dependency of\n"; + + if (ResolveStack == null || !ResolveStack.Any()) + return message; + + for (int i = ResolveStack.Count - 1; i >= 1 ; i--) + { + message += $"\t`{ResolveStack[i]}` resolved as dependency of\n"; + } + + message += $"\t`{ResolveStack[0]}` which is the root type being resolved."; + return message; + } + } + } +} \ No newline at end of file diff --git a/LightweightIocContainer/Interfaces/IIocContainer.cs b/LightweightIocContainer/Interfaces/IIocContainer.cs index 18a7972..afa8af9 100644 --- a/LightweightIocContainer/Interfaces/IIocContainer.cs +++ b/LightweightIocContainer/Interfaces/IIocContainer.cs @@ -77,15 +77,6 @@ namespace LightweightIocContainer.Interfaces /// An instance of the given T Resolve(params object[] arguments); - /// - /// Gets an instance of the given - /// - /// The given - /// The constructor arguments - /// An instance of the given - /// Could not find function - object Resolve(Type type, object[] arguments); - /// /// Clear the multiton instances of the given from the registered multitons list /// diff --git a/LightweightIocContainer/IocContainer.cs b/LightweightIocContainer/IocContainer.cs index 9ceee68..f828caf 100644 --- a/LightweightIocContainer/IocContainer.cs +++ b/LightweightIocContainer/IocContainer.cs @@ -163,9 +163,10 @@ namespace LightweightIocContainer /// /// The given /// The constructor arguments + /// The current resolve stack /// An instance of the given /// Could not find function - public object Resolve(Type type, object[] arguments) + private object Resolve(Type type, object[] arguments, List resolveStack) { MethodInfo resolveMethod = typeof(IocContainer).GetMethod(nameof(ResolveInternal), BindingFlags.NonPublic | BindingFlags.Instance); MethodInfo genericResolveMethod = resolveMethod?.MakeGenericMethod(type); @@ -173,7 +174,14 @@ namespace LightweightIocContainer if (genericResolveMethod == null) throw new InternalResolveException($"Could not find function {nameof(ResolveInternal)}"); - return genericResolveMethod.Invoke(this, new object[] {arguments}); + try //exceptions thrown by methods called with invoke are wrapped into another exception, the exception thrown by the invoked method can be returned by `Exception.GetBaseException()` + { + return genericResolveMethod.Invoke(this, new object[] { arguments, resolveStack }); + } + catch (Exception ex) + { + throw ex.GetBaseException(); + } } /// @@ -181,15 +189,24 @@ namespace LightweightIocContainer /// /// The registered /// The constructor arguments + /// The current resolve stack /// An instance of the given registered /// The given is not registered in this /// The registration for the given has an unknown - private T ResolveInternal(params object[] arguments) + private T ResolveInternal(object[] arguments, List resolveStack = null) { IRegistrationBase registration = _registrations.FirstOrDefault(r => r.InterfaceType == typeof(T)); if (registration == null) throw new TypeNotRegisteredException(typeof(T)); + //Circular dependency check + if (resolveStack == null) //first resolve call + resolveStack = new List {typeof(T)}; //create new stack and add the currently resolving type to the stack + else if (resolveStack.Contains(typeof(T))) + throw new CircularDependencyException(typeof(T), resolveStack); //currently resolving type is still resolving -> circular dependency + else //not the first resolve call in chain but no circular dependencies for now + resolveStack.Add(typeof(T)); //add currently resolving type to the stack + if (registration is IUnitTestCallbackRegistration unitTestCallbackRegistration) { return unitTestCallbackRegistration.UnitTestResolveCallback.Invoke(arguments); @@ -197,11 +214,11 @@ namespace LightweightIocContainer else if (registration is IDefaultRegistration defaultRegistration) { if (defaultRegistration.Lifestyle == Lifestyle.Singleton) - return GetOrCreateSingletonInstance(defaultRegistration, arguments); + return GetOrCreateSingletonInstance(defaultRegistration, arguments, resolveStack); else if (defaultRegistration is IMultitonRegistration multitonRegistration && defaultRegistration.Lifestyle == Lifestyle.Multiton) - return GetOrCreateMultitonInstance(multitonRegistration, arguments); + return GetOrCreateMultitonInstance(multitonRegistration, arguments, resolveStack); - return CreateInstance(defaultRegistration, arguments); + return CreateInstance(defaultRegistration, arguments, resolveStack); } else if (registration is ITypedFactoryRegistration typedFactoryRegistration) { @@ -217,8 +234,9 @@ namespace LightweightIocContainer /// The given /// The registration of the given /// The arguments to resolve + /// The current resolve stack /// An existing or newly created singleton instance of the given - private T GetOrCreateSingletonInstance(IDefaultRegistration registration, params object[] arguments) + private T GetOrCreateSingletonInstance(IDefaultRegistration registration, object[] arguments, List resolveStack) { //if a singleton instance exists return it object instance = _singletons.FirstOrDefault(s => s.type == typeof(T)).instance; @@ -226,7 +244,7 @@ namespace LightweightIocContainer return (T) instance; //if it doesn't already exist create a new instance and add it to the list - T newInstance = CreateInstance(registration, arguments); + T newInstance = CreateInstance(registration, arguments, resolveStack); _singletons.Add((typeof(T), newInstance)); return newInstance; @@ -238,10 +256,11 @@ namespace LightweightIocContainer /// The given /// The registration of the given /// The arguments to resolve + /// The current resolve stack /// An existing or newly created multiton instance of the given /// No arguments given /// Scope argument not given - private T GetOrCreateMultitonInstance(IMultitonRegistration registration, params object[] arguments) + private T GetOrCreateMultitonInstance(IMultitonRegistration registration, object[] arguments, List resolveStack) { if (arguments == null || !arguments.Any()) throw new MultitonResolveException("Can not resolve multiton without arguments.", typeof(T)); @@ -257,13 +276,13 @@ namespace LightweightIocContainer if (instances.TryGetValue(scopeArgument, out object instance)) return (T) instance; - T createdInstance = CreateInstance(registration, arguments); + T createdInstance = CreateInstance(registration, arguments, resolveStack); instances.Add(scopeArgument, createdInstance); return createdInstance; } - T newInstance = CreateInstance(registration, arguments); + T newInstance = CreateInstance(registration, arguments, resolveStack); ConditionalWeakTable weakTable = new ConditionalWeakTable(); weakTable.Add(scopeArgument, newInstance); @@ -279,10 +298,11 @@ namespace LightweightIocContainer /// The given /// The registration of the given /// The constructor arguments + /// The current resolve stack /// A newly created instance of the given - private T CreateInstance(IDefaultRegistration registration, params object[] arguments) + private T CreateInstance(IDefaultRegistration registration, object[] arguments, List resolveStack) { - arguments = ResolveConstructorArguments(registration.ImplementationType, arguments); + arguments = ResolveConstructorArguments(registration.ImplementationType, arguments, resolveStack); T instance = (T) Activator.CreateInstance(registration.ImplementationType, arguments); registration.OnCreateAction?.Invoke(instance); //TODO: Allow async OnCreateAction? @@ -294,10 +314,11 @@ namespace LightweightIocContainer /// /// The that will be created /// The existing arguments + /// The current resolve stack /// An array of all needed constructor arguments to create the /// No matching constructor was found for the given or resolvable arguments [CanBeNull] - private object[] ResolveConstructorArguments(Type type, object[] arguments) + private object[] ResolveConstructorArguments(Type type, object[] arguments, List resolveStack) { //find best ctor List sortedConstructors = type.GetConstructors().OrderByDescending(c => c.GetParameters().Length).ToList(); @@ -319,7 +340,8 @@ namespace LightweightIocContainer object fittingArgument = new InternalResolvePlaceholder(); if (argumentsList != null) { - fittingArgument = argumentsList.FirstOrGiven(a => a?.GetType() == parameter.ParameterType || parameter.ParameterType.IsInstanceOfType(a)); + fittingArgument = argumentsList.FirstOrGiven(a => + a?.GetType() == parameter.ParameterType || parameter.ParameterType.IsInstanceOfType(a)); if (!(fittingArgument is InternalResolvePlaceholder)) { int index = argumentsList.IndexOf(fittingArgument); @@ -329,7 +351,7 @@ namespace LightweightIocContainer { try { - fittingArgument = Resolve(parameter.ParameterType, null); + fittingArgument = Resolve(parameter.ParameterType, null, resolveStack); } catch (Exception) { @@ -347,13 +369,17 @@ namespace LightweightIocContainer if (fittingArgument is InternalResolvePlaceholder && parameter.HasDefaultValue) ctorParams.Add(parameter.DefaultValue); else if (fittingArgument is InternalResolvePlaceholder) - ctorParams.Add(Resolve(parameter.ParameterType, null)); + ctorParams.Add(Resolve(parameter.ParameterType, null, resolveStack)); else ctorParams.Add(fittingArgument); } return ctorParams.ToArray(); } + catch (CircularDependencyException) //don't handle circular dependencies as no matching constructor, just rethrow them + { + throw; + } catch (Exception ex) { if (noMatchingConstructorFoundException == null) diff --git a/LightweightIocContainer/LightweightIocContainer.xml b/LightweightIocContainer/LightweightIocContainer.xml index 69c7a7d..e0c48cb 100644 --- a/LightweightIocContainer/LightweightIocContainer.xml +++ b/LightweightIocContainer/LightweightIocContainer.xml @@ -33,6 +33,33 @@ A function to test each element for a condition The first element of the or a new instance of the given when no element is found + + + A circular dependency was detected during + + + + + A circular dependency was detected during + + The currently resolving + The resolve stack at the time the was thrown + + + + The currently resolving + + + + + The resolve stack at the time the was thrown + + + + + The exception message + + The constructor does not match the given or resolvable arguments @@ -334,15 +361,6 @@ The constructor arguments An instance of the given - - - Gets an instance of the given - - The given - The constructor arguments - An instance of the given - Could not find function - Clear the multiton instances of the given from the registered multitons list @@ -534,60 +552,66 @@ The constructor arguments An instance of the given - + Gets an instance of the given The given The constructor arguments + The current resolve stack An instance of the given - Could not find function + Could not find function - + Gets an instance of a given registered The registered The constructor arguments + The current resolve stack An instance of the given registered The given is not registered in this The registration for the given has an unknown - + Gets or creates a singleton instance of a given The given The registration of the given The arguments to resolve + The current resolve stack An existing or newly created singleton instance of the given - + Gets or creates a multiton instance of a given The given The registration of the given The arguments to resolve + The current resolve stack An existing or newly created multiton instance of the given No arguments given Scope argument not given - + Creates an instance of a given The given The registration of the given The constructor arguments + The current resolve stack A newly created instance of the given - + Resolve the missing constructor arguments The that will be created The existing arguments + The current resolve stack An array of all needed constructor arguments to create the No matching constructor was found for the given or resolvable arguments diff --git a/Test.LightweightIocContainer/IocContainerRecursionTest.cs b/Test.LightweightIocContainer/IocContainerRecursionTest.cs new file mode 100644 index 0000000..3b1df3d --- /dev/null +++ b/Test.LightweightIocContainer/IocContainerRecursionTest.cs @@ -0,0 +1,91 @@ +// Author: Gockner, Simon +// Created: 2019-11-05 +// Copyright(c) 2019 SimonG. All Rights Reserved. + +using JetBrains.Annotations; +using LightweightIocContainer; +using LightweightIocContainer.Exceptions; +using LightweightIocContainer.Interfaces; +using Moq; +using NUnit.Framework; + +namespace Test.LightweightIocContainer +{ + [TestFixture] + public class IocContainerRecursionTest + { + #region TestClasses + + [UsedImplicitly] + public interface IFoo + { + + } + + [UsedImplicitly] + public interface IBar + { + + } + + [UsedImplicitly] + private class Foo : IFoo + { + public Foo(IBar bar) + { + } + } + + [UsedImplicitly] + private class Bar : IBar + { + public Bar(IFoo foo) + { + } + } + + #endregion TestClasses + + + + private IIocContainer _iocContainer; + + [SetUp] + public void SetUp() + { + _iocContainer = new IocContainer(); + + _iocContainer.Register(); + _iocContainer.Register(); + } + + [TearDown] + public void TearDown() + { + _iocContainer.Dispose(); + } + + [Test] + public void TestCircularDependencies() + { + CircularDependencyException exception = Assert.Throws(() => _iocContainer.Resolve()); + Assert.AreEqual(typeof(IFoo), exception.ResolvingType); + Assert.AreEqual(2, exception.ResolveStack.Count); + + string message = $"Circular dependency has been detected when trying to resolve `{typeof(IFoo)}`.\n" + + "Resolve stack that resulted in the circular dependency:\n" + + $"\t`{typeof(IFoo)}` resolved as dependency of\n" + + $"\t`{typeof(IBar)}` resolved as dependency of\n" + + $"\t`{typeof(IFoo)}` which is the root type being resolved."; + + Assert.AreEqual(message, exception.Message); + } + + [Test] + public void TestRecursionWithParam() + { + Assert.DoesNotThrow(() => _iocContainer.Resolve(new Mock().Object)); + Assert.DoesNotThrow(() => _iocContainer.Resolve(new Mock().Object)); + } + } +} \ No newline at end of file diff --git a/Test.LightweightIocContainer/IocContainerTest.cs b/Test.LightweightIocContainer/IocContainerTest.cs index d667c7c..1658ab8 100644 --- a/Test.LightweightIocContainer/IocContainerTest.cs +++ b/Test.LightweightIocContainer/IocContainerTest.cs @@ -238,16 +238,6 @@ namespace Test.LightweightIocContainer Assert.IsInstanceOf(resolvedTest); } - [Test] - public void TestResolveReflection() - { - _iocContainer.Register(); - - object resolvedTest = _iocContainer.Resolve(typeof(ITest), null); - - Assert.IsInstanceOf(resolvedTest); - } - [Test] public void TestResolveSingleton() { @@ -309,14 +299,16 @@ namespace Test.LightweightIocContainer public void TestResolveNoMatchingConstructor() { _iocContainer.Register(); - Assert.Throws(() => _iocContainer.Resolve()); + NoMatchingConstructorFoundException exception = Assert.Throws(() => _iocContainer.Resolve()); + Assert.AreEqual(typeof(TestConstructor), exception.Type); } [Test] public void TestResolvePrivateConstructor() { _iocContainer.Register(); - Assert.Throws(() => _iocContainer.Resolve()); + NoPublicConstructorFoundException exception = Assert.Throws(() => _iocContainer.Resolve()); + Assert.AreEqual(typeof(TestPrivateConstructor), exception.Type); } [Test]