diff --git a/src/TinyIoC.Tests/TestData/BasicClasses.cs b/src/TinyIoC.Tests/TestData/BasicClasses.cs index bb7c820..2b2cce9 100644 --- a/src/TinyIoC.Tests/TestData/BasicClasses.cs +++ b/src/TinyIoC.Tests/TestData/BasicClasses.cs @@ -16,7 +16,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; +using System.Runtime.InteropServices; namespace TinyIoC.Tests.TestData { @@ -193,6 +193,104 @@ public GenericClassWithInterface(I prop1, S prop2) } } + internal class TestClassWithOptionalNullParameter + { + public TestClassNoInterfaceDefaultCtor OptionalReferenceTypeParameter { get; private set; } + + public TestClassWithOptionalNullParameter(TestClassNoInterfaceDefaultCtor optionalReferenceTypeParameter = null) + { + OptionalReferenceTypeParameter = optionalReferenceTypeParameter; + } + } + + internal class TestClassWithDependentOptionalNullParameter + { + public TestClassWithOptionalStringParameter OptionalReferenceTypeParameter { get; private set; } + + public TestClassWithDependentOptionalNullParameter(TestClassWithOptionalStringParameter optionalReferenceTypeParameter = null) + { + OptionalReferenceTypeParameter = optionalReferenceTypeParameter; + } + } + + internal class TestClassWithOptionalValueTypeParameter + { + public DateTime OptionalValueTypeParameter { get; private set; } + + public TestClassWithOptionalValueTypeParameter(DateTime optionalValueTypeParameter = default(DateTime)) + { + OptionalValueTypeParameter = optionalValueTypeParameter; + } + } + + internal class TestClassWithOptionalIntParameter + { + public const int DefaultIntParameterValue = 10; + public int OptionalIntParameter { get; private set; } + + public TestClassWithOptionalIntParameter(int optionalIntParameter = DefaultIntParameterValue) + { + OptionalIntParameter = optionalIntParameter; + } + } + + internal class TestClassWithOptionalIntParameterViaAttribute + { + public const int DefaultIntParameterValue = 100; + public int OptionalIntParameter { get; private set; } + + public TestClassWithOptionalIntParameterViaAttribute([Optional, DefaultParameterValue(DefaultIntParameterValue)]int optionalIntParameter) + { + OptionalIntParameter = optionalIntParameter; + } + } + + internal class TestClassWithOptionalDefaultIntParameterViaAttribute + { + public const int DefaultIntParameterValue = default(int); + public int OptionalIntParameter { get; private set; } + + public TestClassWithOptionalDefaultIntParameterViaAttribute([Optional, DefaultParameterValue(default(int))]int optionalIntParameter) + { + OptionalIntParameter = optionalIntParameter; + } + } + + internal class TestClassWithOptionalStringParameter + { + public const string DefaultStringParameterValue = "Default string"; + + public string OptionalStringParameter { get; private set; } + + public TestClassWithOptionalStringParameter(string optionalStringParameter = DefaultStringParameterValue) + { + OptionalStringParameter = optionalStringParameter; + } + } + + internal class TestClassWithOptionalStringParameterViaAttribute + { + public const string DefaultStringParameterValue = "Default string via attribute"; + + public string OptionalStringParameter { get; private set; } + + public TestClassWithOptionalStringParameterViaAttribute([Optional, DefaultParameterValue(DefaultStringParameterValue)]string optionalStringParameter) + { + OptionalStringParameter = optionalStringParameter; + } + } + + internal class TestClassWithOptionalNullStringParameterViaAttribute + { + public const string DefaultStringParameterValue = default(string); + + public string OptionalStringParameter { get; private set; } + + public TestClassWithOptionalNullStringParameterViaAttribute([Optional, DefaultParameterValue(default(string))]string optionalStringParameter) + { + OptionalStringParameter = optionalStringParameter; + } + } internal class GenericClassWithParametersAndDependencies { diff --git a/src/TinyIoC.Tests/TinyIoCTests.cs b/src/TinyIoC.Tests/TinyIoCTests.cs index cd08869..ada353b 100644 --- a/src/TinyIoC.Tests/TinyIoCTests.cs +++ b/src/TinyIoC.Tests/TinyIoCTests.cs @@ -1625,6 +1625,135 @@ public void Resolve_ConstructorSpecifiedThatRequiresParametersButNonePassed_Fail //Assert.IsTrue(false); } + [TestMethod] + public void Resolve_ConstructorWithOptionalNullParameter_Resolves() + { + var container = UtilityMethods.GetContainer(); + container.Register(); + + var result = container.Resolve(); + + Assert.IsNotNull(result); + } + + [TestMethod] + public void Resolve_ConstructorWithDependentOptionalNullParameter_Resolves() + { + var container = UtilityMethods.GetContainer(); + container.Register(); + + var result = container.Resolve(); + Assert.IsTrue(result.OptionalReferenceTypeParameter.OptionalStringParameter == TestClassWithOptionalStringParameter.DefaultStringParameterValue); + } + + [TestMethod] + public void Resolve_ConstructorWithOptionalValueTypeParameter_Resolves() + { + var container = UtilityMethods.GetContainer(); + container.Register(); + + var result = container.Resolve(); + + Assert.IsTrue(result.OptionalValueTypeParameter == default(DateTime)); + } + + [TestMethod] + public void Resolve_ConstructorWithOptionaInt10Parameter_Resolves() + { + var container = UtilityMethods.GetContainer(); + container.Register(); + + var result = container.Resolve(); + + Assert.IsTrue(result.OptionalIntParameter == 10); + } + + [TestMethod] + public void Resolve_ConstructorWithOptionalStringParameter_Resolves() + { + var container = UtilityMethods.GetContainer(); + container.Register(); + + var result = container.Resolve(); + + Assert.IsTrue(result.OptionalStringParameter == "Default string"); + } + + [TestMethod] + public void Resolve_ConstructorWithOptionaIntParameterViaAttribute_Resolves() + { + var container = UtilityMethods.GetContainer(); + container.Register(); + + var result = container.Resolve(); + + Assert.IsTrue(result.OptionalIntParameter == TestClassWithOptionalIntParameterViaAttribute.DefaultIntParameterValue); + } + + [TestMethod] + public void Resolve_ConstructorWithOptionaDefaultIntParameterViaAttribute_Resolves() + { + var container = UtilityMethods.GetContainer(); + container.Register(); + + var result = container.Resolve(); + + Assert.IsTrue(result.OptionalIntParameter == TestClassWithOptionalDefaultIntParameterViaAttribute.DefaultIntParameterValue); + } + + /// + /// Check that a default value is used only if a parameter value is not explicitly set. + /// + [TestMethod] + public void Resolve_ConstructorWithOptionaNonDefaultIntParameter_Resolves() + { + const int value = 200; + var container = UtilityMethods.GetContainer(); + container.Register(new TestClassWithOptionalIntParameter(value)); + + var result = container.Resolve(); + + Assert.IsTrue(result.OptionalIntParameter == value); + } + + /// + /// Check that a default value is used only if a parameter value is not explicitly set. + /// + [TestMethod] + public void Resolve_ConstructorWithOptionaNonDefaultStringParameter_Resolves() + { + const string value = "Non default string value"; + var container = UtilityMethods.GetContainer(); + container.Register(new TestClassWithOptionalStringParameter(value)); + + var result = container.Resolve(); + + Assert.IsTrue(result.OptionalStringParameter == value); + } + + [TestMethod] + public void Resolve_ConstructorWithOptionalStringParameterViaAttribute_Resolves() + { + var container = UtilityMethods.GetContainer(); + container.Register(); + + var result = container.Resolve(); + + Assert.IsTrue(result.OptionalStringParameter == TestClassWithOptionalStringParameterViaAttribute.DefaultStringParameterValue); + } + + [TestMethod] + public void Resolve_ConstructorWithOptionalNullStringParameterViaAttribute_Resolves() + { + var container = UtilityMethods.GetContainer(); + container.Register(); + + var result = container.Resolve(); + + Assert.IsTrue(result.OptionalStringParameter == TestClassWithOptionalNullStringParameterViaAttribute.DefaultStringParameterValue); + } + + [TestMethod] public void CanResolve_ConstructorSpecifiedThatRequiresParametersButNonePassed_ReturnsFalse() { @@ -1637,6 +1766,61 @@ public void CanResolve_ConstructorSpecifiedThatRequiresParametersButNonePassed_R Assert.IsFalse(result); } + [TestMethod] + public void CanResolve_ConstructorWithOptionalNullParameter_ReturnsTrue() + { + var container = UtilityMethods.GetContainer(); + container.Register(); + + var result = container.CanResolve(); + + Assert.IsTrue(result); + } + + [TestMethod] + public void CanResolve_ConstructorWithDependentOptionalNullParameter_ReturnsTrue() + { + var container = UtilityMethods.GetContainer(); + container.Register(); + + var result = container.CanResolve(); + + Assert.IsTrue(result); + } + + [TestMethod] + public void CanResolve_ConstructorWithOptionalValueTypeParameter_ReturnsTrue() + { + var container = UtilityMethods.GetContainer(); + container.Register(); + + var result = container.CanResolve(); + + Assert.IsTrue(result); + } + + [TestMethod] + public void CanResolve_ConstructorWithOptionaInt10Parameter_ReturnsTrue() + { + var container = UtilityMethods.GetContainer(); + container.Register(); + + var result = container.CanResolve(); + + Assert.IsTrue(result); + } + + [TestMethod] + public void CanResolve_ConstructorWithOptionalStringParameter_ReturnsTrue() + { + var container = UtilityMethods.GetContainer(); + container.Register(); + + var result = container.CanResolve(); + + Assert.IsTrue(result); + } + [TestMethod] public void CanResolve_SingletonFactoryConstructorSpecified_ReturnsTrue() { diff --git a/src/TinyIoC/TinyIoC.cs b/src/TinyIoC/TinyIoC.cs index 388b2e3..3121f77 100644 --- a/src/TinyIoC/TinyIoC.cs +++ b/src/TinyIoC/TinyIoC.cs @@ -3926,6 +3926,9 @@ private bool CanConstruct(ConstructorInfo ctor, NamedParameterOverloads paramete foreach (var parameter in ctor.GetParameters()) { + if (parameter.IsOptional) + return true; + if (string.IsNullOrEmpty(parameter.Name)) return false; @@ -3994,6 +3997,8 @@ private object ConstructType(Type requestedType, Type implementationType, NamedP return ConstructType(requestedType, implementationType, null, parameters, options); } + private static readonly SafeDictionary _DefaultValues = new SafeDictionary(); + private object ConstructType(Type requestedType, Type implementationType, ConstructorInfo constructor, NamedParameterOverloads parameters, ResolveOptions options) { var typeToConstruct = implementationType; @@ -4028,12 +4033,39 @@ private object ConstructType(Type requestedType, Type implementationType, Constr try { - args[parameterIndex] = parameters.ContainsKey(currentParam.Name) ? - parameters[currentParam.Name] : - ResolveInternal( - new TypeRegistration(currentParam.ParameterType), - NamedParameterOverloads.Default, - options); + object parameterValue; + if (parameters.TryGetValue(currentParam.Name, out parameterValue)) + { + args[parameterIndex] = parameterValue; + } + else + { + if (currentParam.IsOptional && (currentParam.ParameterType == typeof(string) || currentParam.ParameterType.IsValueType())) + { + try + { + args[parameterIndex] = currentParam.DefaultValue; + } + catch + { + // The currentParam.DefaultValue is not always valid, e.g. with default(DateTime), we get a 'SystemFormatException'- possibly due to culture differences. + // If so, Activator.CreateInstance(Type) *should* always work. + object defaultValue; + if (!_DefaultValues.TryGetValue(currentParam.ParameterType, out defaultValue)) + { + // Potentially creating 'defaultValue' twice in multi-threaded writes isn't a problem; all instances arae equivalent and it'll only happen once, + // so even if Activator.CreateInstance(Type) were unpleasantly slow, it wouldn't really matter. + defaultValue = Activator.CreateInstance(currentParam.ParameterType); + _DefaultValues[currentParam.ParameterType] = defaultValue; + } + args[parameterIndex] = defaultValue; + } + } + else + { + args[parameterIndex] = ResolveInternal(new TypeRegistration(currentParam.ParameterType), NamedParameterOverloads.Default, options); + } + } } catch (TinyIoCResolutionException ex) {