#28: add CircularDependencyException:

- throw circularDependencyException when user tries to resolve a circular dependency
- make non-generic Resolve() method private
- add resolveStack to private Resolve() and ResolveInternal<>() methods and all methods in between their calls
pull/32/head
Simon Gockner 6 years ago
parent d7b3a5eb99
commit a42188f687
  1. 63
      LightweightIocContainer/Exceptions/CircularDependencyException.cs
  2. 9
      LightweightIocContainer/Interfaces/IIocContainer.cs
  3. 60
      LightweightIocContainer/IocContainer.cs
  4. 56
      LightweightIocContainer/LightweightIocContainer.xml
  5. 91
      Test.LightweightIocContainer/IocContainerRecursionTest.cs
  6. 16
      Test.LightweightIocContainer/IocContainerTest.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
{
/// <summary>
/// A circular dependency was detected during <see cref="IIocContainer.Resolve{T}()"/>
/// </summary>
internal class CircularDependencyException : Exception
{
/// <summary>
/// A circular dependency was detected during <see cref="IIocContainer.Resolve{T}()"/>
/// </summary>
/// <param name="resolvingType">The currently resolving <see cref="Type"/></param>
/// <param name="resolveStack">The resolve stack at the time the <see cref="CircularDependencyException"/> was thrown</param>
public CircularDependencyException(Type resolvingType, List<Type> resolveStack)
{
ResolvingType = resolvingType;
ResolveStack = resolveStack;
}
/// <summary>
/// The currently resolving <see cref="Type"/>
/// </summary>
public Type ResolvingType { get; }
/// <summary>
/// The resolve stack at the time the <see cref="CircularDependencyException"/> was thrown
/// </summary>
public List<Type> ResolveStack { get; }
/// <summary>
/// The exception message
/// </summary>
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;
}
}
}
}

@ -77,15 +77,6 @@ namespace LightweightIocContainer.Interfaces
/// <returns>An instance of the given <see cref="Type"/></returns>
T Resolve<T>(params object[] arguments);
/// <summary>
/// Gets an instance of the given <see cref="Type"/>
/// </summary>
/// <param name="type">The given <see cref="Type"/></param>
/// <param name="arguments">The constructor arguments</param>
/// <returns>An instance of the given <see cref="Type"/></returns>
/// <exception cref="InternalResolveException">Could not find function <see cref="IocContainer.ResolveInternal{T}"/></exception>
object Resolve(Type type, object[] arguments);
/// <summary>
/// Clear the multiton instances of the given <see cref="Type"/> from the registered multitons list
/// </summary>

@ -163,9 +163,10 @@ namespace LightweightIocContainer
/// </summary>
/// <param name="type">The given <see cref="Type"/></param>
/// <param name="arguments">The constructor arguments</param>
/// <param name="resolveStack">The current resolve stack</param>
/// <returns>An instance of the given <see cref="Type"/></returns>
/// <exception cref="InternalResolveException">Could not find function <see cref="ResolveInternal{T}"/></exception>
public object Resolve(Type type, object[] arguments)
private object Resolve(Type type, object[] arguments, List<Type> 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();
}
}
/// <summary>
@ -181,15 +189,24 @@ namespace LightweightIocContainer
/// </summary>
/// <typeparam name="T">The registered <see cref="Type"/></typeparam>
/// <param name="arguments">The constructor arguments</param>
/// <param name="resolveStack">The current resolve stack</param>
/// <returns>An instance of the given registered <see cref="Type"/></returns>
/// <exception cref="TypeNotRegisteredException">The given <see cref="Type"/> is not registered in this <see cref="IocContainer"/></exception>
/// <exception cref="UnknownRegistrationException">The registration for the given <see cref="Type"/> has an unknown <see cref="Type"/></exception>
private T ResolveInternal<T>(params object[] arguments)
private T ResolveInternal<T>(object[] arguments, List<Type> 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<Type> {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<T> unitTestCallbackRegistration)
{
return unitTestCallbackRegistration.UnitTestResolveCallback.Invoke(arguments);
@ -197,11 +214,11 @@ namespace LightweightIocContainer
else if (registration is IDefaultRegistration<T> defaultRegistration)
{
if (defaultRegistration.Lifestyle == Lifestyle.Singleton)
return GetOrCreateSingletonInstance(defaultRegistration, arguments);
return GetOrCreateSingletonInstance(defaultRegistration, arguments, resolveStack);
else if (defaultRegistration is IMultitonRegistration<T> 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<T> typedFactoryRegistration)
{
@ -217,8 +234,9 @@ namespace LightweightIocContainer
/// <typeparam name="T">The given <see cref="Type"/></typeparam>
/// <param name="registration">The registration of the given <see cref="Type"/></param>
/// <param name="arguments">The arguments to resolve</param>
/// <param name="resolveStack">The current resolve stack</param>
/// <returns>An existing or newly created singleton instance of the given <see cref="Type"/></returns>
private T GetOrCreateSingletonInstance<T>(IDefaultRegistration<T> registration, params object[] arguments)
private T GetOrCreateSingletonInstance<T>(IDefaultRegistration<T> registration, object[] arguments, List<Type> 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
/// <typeparam name="T">The given <see cref="Type"/></typeparam>
/// <param name="registration">The registration of the given <see cref="Type"/></param>
/// <param name="arguments">The arguments to resolve</param>
/// <param name="resolveStack">The current resolve stack</param>
/// <returns>An existing or newly created multiton instance of the given <see cref="Type"/></returns>
/// <exception cref="MultitonResolveException">No arguments given</exception>
/// <exception cref="MultitonResolveException">Scope argument not given</exception>
private T GetOrCreateMultitonInstance<T>(IMultitonRegistration<T> registration, params object[] arguments)
private T GetOrCreateMultitonInstance<T>(IMultitonRegistration<T> registration, object[] arguments, List<Type> 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<object, object> weakTable = new ConditionalWeakTable<object, object>();
weakTable.Add(scopeArgument, newInstance);
@ -279,10 +298,11 @@ namespace LightweightIocContainer
/// <typeparam name="T">The given <see cref="Type"/></typeparam>
/// <param name="registration">The registration of the given <see cref="Type"/></param>
/// <param name="arguments">The constructor arguments</param>
/// <param name="resolveStack">The current resolve stack</param>
/// <returns>A newly created instance of the given <see cref="Type"/></returns>
private T CreateInstance<T>(IDefaultRegistration<T> registration, params object[] arguments)
private T CreateInstance<T>(IDefaultRegistration<T> registration, object[] arguments, List<Type> 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
/// </summary>
/// <param name="type">The <see cref="Type"/> that will be created</param>
/// <param name="arguments">The existing arguments</param>
/// <param name="resolveStack">The current resolve stack</param>
/// <returns>An array of all needed constructor arguments to create the <see cref="Type"/></returns>
/// <exception cref="NoMatchingConstructorFoundException">No matching constructor was found for the given or resolvable arguments</exception>
[CanBeNull]
private object[] ResolveConstructorArguments(Type type, object[] arguments)
private object[] ResolveConstructorArguments(Type type, object[] arguments, List<Type> resolveStack)
{
//find best ctor
List<ConstructorInfo> 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<object, InternalResolvePlaceholder>(a => a?.GetType() == parameter.ParameterType || parameter.ParameterType.IsInstanceOfType(a));
fittingArgument = argumentsList.FirstOrGiven<object, InternalResolvePlaceholder>(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)

@ -33,6 +33,33 @@
<param name="predicate">A function to test each element for a condition</param>
<returns>The first element of the <see cref="T:System.Collections.Generic.IEnumerable`1"/> or a new instance of the given <see cref="T:System.Type"/> when no element is found</returns>
</member>
<member name="T:LightweightIocContainer.Exceptions.CircularDependencyException">
<summary>
A circular dependency was detected during <see cref="M:LightweightIocContainer.Interfaces.IIocContainer.Resolve``1"/>
</summary>
</member>
<member name="M:LightweightIocContainer.Exceptions.CircularDependencyException.#ctor(System.Type,System.Collections.Generic.List{System.Type})">
<summary>
A circular dependency was detected during <see cref="M:LightweightIocContainer.Interfaces.IIocContainer.Resolve``1"/>
</summary>
<param name="resolvingType">The currently resolving <see cref="T:System.Type"/></param>
<param name="resolveStack">The resolve stack at the time the <see cref="T:LightweightIocContainer.Exceptions.CircularDependencyException"/> was thrown</param>
</member>
<member name="P:LightweightIocContainer.Exceptions.CircularDependencyException.ResolvingType">
<summary>
The currently resolving <see cref="T:System.Type"/>
</summary>
</member>
<member name="P:LightweightIocContainer.Exceptions.CircularDependencyException.ResolveStack">
<summary>
The resolve stack at the time the <see cref="T:LightweightIocContainer.Exceptions.CircularDependencyException"/> was thrown
</summary>
</member>
<member name="P:LightweightIocContainer.Exceptions.CircularDependencyException.Message">
<summary>
The exception message
</summary>
</member>
<member name="T:LightweightIocContainer.Exceptions.ConstructorNotMatchingException">
<summary>
The constructor does not match the given or resolvable arguments
@ -334,15 +361,6 @@
<param name="arguments">The constructor arguments</param>
<returns>An instance of the given <see cref="T:System.Type"/></returns>
</member>
<member name="M:LightweightIocContainer.Interfaces.IIocContainer.Resolve(System.Type,System.Object[])">
<summary>
Gets an instance of the given <see cref="T:System.Type"/>
</summary>
<param name="type">The given <see cref="T:System.Type"/></param>
<param name="arguments">The constructor arguments</param>
<returns>An instance of the given <see cref="T:System.Type"/></returns>
<exception cref="T:LightweightIocContainer.Exceptions.InternalResolveException">Could not find function <see cref="M:LightweightIocContainer.IocContainer.ResolveInternal``1(System.Object[])"/></exception>
</member>
<member name="M:LightweightIocContainer.Interfaces.IIocContainer.ClearMultitonInstances``1">
<summary>
Clear the multiton instances of the given <see cref="T:System.Type"/> from the registered multitons list
@ -534,60 +552,66 @@
<param name="arguments">The constructor arguments</param>
<returns>An instance of the given <see cref="T:System.Type"/></returns>
</member>
<member name="M:LightweightIocContainer.IocContainer.Resolve(System.Type,System.Object[])">
<member name="M:LightweightIocContainer.IocContainer.Resolve(System.Type,System.Object[],System.Collections.Generic.List{System.Type})">
<summary>
Gets an instance of the given <see cref="T:System.Type"/>
</summary>
<param name="type">The given <see cref="T:System.Type"/></param>
<param name="arguments">The constructor arguments</param>
<param name="resolveStack">The current resolve stack</param>
<returns>An instance of the given <see cref="T:System.Type"/></returns>
<exception cref="T:LightweightIocContainer.Exceptions.InternalResolveException">Could not find function <see cref="M:LightweightIocContainer.IocContainer.ResolveInternal``1(System.Object[])"/></exception>
<exception cref="T:LightweightIocContainer.Exceptions.InternalResolveException">Could not find function <see cref="M:LightweightIocContainer.IocContainer.ResolveInternal``1(System.Object[],System.Collections.Generic.List{System.Type})"/></exception>
</member>
<member name="M:LightweightIocContainer.IocContainer.ResolveInternal``1(System.Object[])">
<member name="M:LightweightIocContainer.IocContainer.ResolveInternal``1(System.Object[],System.Collections.Generic.List{System.Type})">
<summary>
Gets an instance of a given registered <see cref="T:System.Type"/>
</summary>
<typeparam name="T">The registered <see cref="T:System.Type"/></typeparam>
<param name="arguments">The constructor arguments</param>
<param name="resolveStack">The current resolve stack</param>
<returns>An instance of the given registered <see cref="T:System.Type"/></returns>
<exception cref="T:LightweightIocContainer.Exceptions.TypeNotRegisteredException">The given <see cref="T:System.Type"/> is not registered in this <see cref="T:LightweightIocContainer.IocContainer"/></exception>
<exception cref="T:LightweightIocContainer.Exceptions.UnknownRegistrationException">The registration for the given <see cref="T:System.Type"/> has an unknown <see cref="T:System.Type"/></exception>
</member>
<member name="M:LightweightIocContainer.IocContainer.GetOrCreateSingletonInstance``1(LightweightIocContainer.Interfaces.Registrations.IDefaultRegistration{``0},System.Object[])">
<member name="M:LightweightIocContainer.IocContainer.GetOrCreateSingletonInstance``1(LightweightIocContainer.Interfaces.Registrations.IDefaultRegistration{``0},System.Object[],System.Collections.Generic.List{System.Type})">
<summary>
Gets or creates a singleton instance of a given <see cref="T:System.Type"/>
</summary>
<typeparam name="T">The given <see cref="T:System.Type"/></typeparam>
<param name="registration">The registration of the given <see cref="T:System.Type"/></param>
<param name="arguments">The arguments to resolve</param>
<param name="resolveStack">The current resolve stack</param>
<returns>An existing or newly created singleton instance of the given <see cref="T:System.Type"/></returns>
</member>
<member name="M:LightweightIocContainer.IocContainer.GetOrCreateMultitonInstance``1(LightweightIocContainer.Interfaces.Registrations.IMultitonRegistration{``0},System.Object[])">
<member name="M:LightweightIocContainer.IocContainer.GetOrCreateMultitonInstance``1(LightweightIocContainer.Interfaces.Registrations.IMultitonRegistration{``0},System.Object[],System.Collections.Generic.List{System.Type})">
<summary>
Gets or creates a multiton instance of a given <see cref="T:System.Type"/>
</summary>
<typeparam name="T">The given <see cref="T:System.Type"/></typeparam>
<param name="registration">The registration of the given <see cref="T:System.Type"/></param>
<param name="arguments">The arguments to resolve</param>
<param name="resolveStack">The current resolve stack</param>
<returns>An existing or newly created multiton instance of the given <see cref="T:System.Type"/></returns>
<exception cref="T:LightweightIocContainer.Exceptions.MultitonResolveException">No arguments given</exception>
<exception cref="T:LightweightIocContainer.Exceptions.MultitonResolveException">Scope argument not given</exception>
</member>
<member name="M:LightweightIocContainer.IocContainer.CreateInstance``1(LightweightIocContainer.Interfaces.Registrations.IDefaultRegistration{``0},System.Object[])">
<member name="M:LightweightIocContainer.IocContainer.CreateInstance``1(LightweightIocContainer.Interfaces.Registrations.IDefaultRegistration{``0},System.Object[],System.Collections.Generic.List{System.Type})">
<summary>
Creates an instance of a given <see cref="T:System.Type"/>
</summary>
<typeparam name="T">The given <see cref="T:System.Type"/></typeparam>
<param name="registration">The registration of the given <see cref="T:System.Type"/></param>
<param name="arguments">The constructor arguments</param>
<param name="resolveStack">The current resolve stack</param>
<returns>A newly created instance of the given <see cref="T:System.Type"/></returns>
</member>
<member name="M:LightweightIocContainer.IocContainer.ResolveConstructorArguments(System.Type,System.Object[])">
<member name="M:LightweightIocContainer.IocContainer.ResolveConstructorArguments(System.Type,System.Object[],System.Collections.Generic.List{System.Type})">
<summary>
Resolve the missing constructor arguments
</summary>
<param name="type">The <see cref="T:System.Type"/> that will be created</param>
<param name="arguments">The existing arguments</param>
<param name="resolveStack">The current resolve stack</param>
<returns>An array of all needed constructor arguments to create the <see cref="T:System.Type"/></returns>
<exception cref="T:LightweightIocContainer.Exceptions.NoMatchingConstructorFoundException">No matching constructor was found for the given or resolvable arguments</exception>
</member>

@ -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<IFoo, Foo>();
_iocContainer.Register<IBar, Bar>();
}
[TearDown]
public void TearDown()
{
_iocContainer.Dispose();
}
[Test]
public void TestCircularDependencies()
{
CircularDependencyException exception = Assert.Throws<CircularDependencyException>(() => _iocContainer.Resolve<IFoo>());
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<IFoo>(new Mock<IBar>().Object));
Assert.DoesNotThrow(() => _iocContainer.Resolve<IBar>(new Mock<IFoo>().Object));
}
}
}

@ -238,16 +238,6 @@ namespace Test.LightweightIocContainer
Assert.IsInstanceOf<TestConstructor>(resolvedTest);
}
[Test]
public void TestResolveReflection()
{
_iocContainer.Register<ITest, Test>();
object resolvedTest = _iocContainer.Resolve(typeof(ITest), null);
Assert.IsInstanceOf<Test>(resolvedTest);
}
[Test]
public void TestResolveSingleton()
{
@ -309,14 +299,16 @@ namespace Test.LightweightIocContainer
public void TestResolveNoMatchingConstructor()
{
_iocContainer.Register<ITest, TestConstructor>();
Assert.Throws<NoMatchingConstructorFoundException>(() => _iocContainer.Resolve<ITest>());
NoMatchingConstructorFoundException exception = Assert.Throws<NoMatchingConstructorFoundException>(() => _iocContainer.Resolve<ITest>());
Assert.AreEqual(typeof(TestConstructor), exception.Type);
}
[Test]
public void TestResolvePrivateConstructor()
{
_iocContainer.Register<ITest, TestPrivateConstructor>();
Assert.Throws<NoPublicConstructorFoundException>(() => _iocContainer.Resolve<ITest>());
NoPublicConstructorFoundException exception = Assert.Throws<NoPublicConstructorFoundException>(() => _iocContainer.Resolve<ITest>());
Assert.AreEqual(typeof(TestPrivateConstructor), exception.Type);
}
[Test]

Loading…
Cancel
Save