diff --git a/src/Spring/Spring.Core/Objects/Factory/Attributes/InitDestroyAttributeObjectPostProcessor.cs b/src/Spring/Spring.Core/Objects/Factory/Attributes/InitDestroyAttributeObjectPostProcessor.cs new file mode 100644 index 00000000..e76446c1 --- /dev/null +++ b/src/Spring/Spring.Core/Objects/Factory/Attributes/InitDestroyAttributeObjectPostProcessor.cs @@ -0,0 +1,296 @@ +#region License + +/* + * Copyright © 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Common.Logging; +using Spring.Objects; +using Spring.Objects.Factory; +using Spring.Objects.Factory.Config; +using Spring.Core; + +namespace Spring.Objects.Factory.Attributes +{ + /// + /// implementation + /// that invokes attributed init and destroy methods. Allows for an attributation + /// alternative to Spring's and + /// callback interfaces. + /// + /// Invoke and destroy annotations may be applied to methods of any visibility: + /// public, protected, or private. Multiple such methods + /// may be annotated, but it is recommended to only annotate one single + /// init method and destroy method, respectively. + /// + public class InitDestroyAttributeObjectPostProcessor : IDestructionAwareObjectPostProcessor, IObjectFactoryAware, IOrdered + { + private static readonly ILog Logger = LogManager.GetLogger(); + + private IConfigurableListableObjectFactory _objectFactory; + private readonly IDictionary _lifecycleMetadataCache; + + private int _order = int.MaxValue; + private Type _initAttributeType; + private Type _destroyAttributeType; + + + /// + /// Return the order value of this object, where a higher value means greater in + /// terms of sorting. + /// + /// + ///

Normally starting with 0 or 1, with indicating + /// greatest. Same order values will result in arbitrary positions for the affected + /// objects. + ///

Higher value can be interpreted as lower priority, consequently the first object + /// has highest priority. + ///

+ ///
+ /// + /// The order value. + /// + public int Order { get { return _order; } private set { _order = value; } } + + /// + /// Specify the init attribute to check for, indicating initialization + /// methods to call after configuration of an object. + /// + public Type InitAttributeType { get { return _initAttributeType; } set { _initAttributeType = value; } } + + /// + /// Specify the destroy attribute to check for, indicating disposal + /// methods to call before object is destroyed + /// + public Type DestroyAttributeType { get { return _destroyAttributeType; } set { _destroyAttributeType = value; } } + + + /// + /// + /// + public IObjectFactory ObjectFactory + { + set { _objectFactory = value as IConfigurableListableObjectFactory; } + } + + /// + /// Creates InitDestroy Post Processor with default attribute types of + /// + /// + public InitDestroyAttributeObjectPostProcessor() + { + _initAttributeType = typeof (PostConstructAttribute); + _destroyAttributeType = typeof (PreDestroyAttribute); + + _lifecycleMetadataCache = new Dictionary(); + } + + /// + /// Applies PostConstruct init method initialisation if instance is attributed + /// + /// The new object instance. + /// The name of the object. + /// + /// + /// The object instance to use, either the original or a wrapped one. + /// + /// In case of errors. + /// + public object PostProcessBeforeInitialization(object instance, string name) + { + try + { + var metadata = FindLifecycleMetadata(instance.GetType(), name); + metadata.InvokeInitMethods(instance, name); + } + catch (Exception e) + { + throw new ObjectCreationException(name, "Couldn't invoke init method", e); + } + + return instance; + } + + /// + /// No special post processing after initialization + /// + public object PostProcessAfterInitialization(object instance, string objectName) + { + return instance; + } + + + /// + /// Executed PreDestroy methods in given order for provided instance + /// + /// The new object instance.The name of the object.In case of errors. + /// + public void PostProcessBeforeDestruction(object instance, string name) + { + try + { + var metadata = FindLifecycleMetadata(instance.GetType(), name); + metadata.InvokeDestroyMethods(instance, name); + } + catch (Exception e) + { + throw new ObjectsException("Couldn't invoke destroy method", e); + } + } + + private LifecycleLifecycleMetadata FindLifecycleMetadata(Type instanceType, string name) + { + if (_lifecycleMetadataCache.ContainsKey(name)) + return _lifecycleMetadataCache[name]; + + lock (_lifecycleMetadataCache) + { + var metadata = BuildLifecycleMetadata(instanceType, name); + _lifecycleMetadataCache.Add(name, metadata); + return metadata; + } + } + + private LifecycleLifecycleMetadata BuildLifecycleMetadata(Type instanceType, string name) + { + var initMethods = new List(); + var destroyMethods = new List(); + + do + { + var curInitMethods = new List(); + var curDestroyMethods = new List(); + var methods = instanceType.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); + foreach (var methodInfo in methods) + { + var initAttribute = + Attribute.GetCustomAttribute(methodInfo, _initAttributeType) as PostConstructAttribute; + if (initAttribute != null && methodInfo.DeclaringType == instanceType) + { + Logger.Debug(m => m("Found init method on class [{0}]: {1}", instanceType.Name, methodInfo.Name)); + curInitMethods.Add(new LifecycleElement(methodInfo, initAttribute.Order)); + } + + var destroyAttribute = + Attribute.GetCustomAttribute(methodInfo, _destroyAttributeType) as PreDestroyAttribute; + if (destroyAttribute != null && methodInfo.DeclaringType == instanceType) + { + Logger.Debug(m => m("Found destroy method on class [{0}]: {1}", instanceType.Name, methodInfo.Name)); + curDestroyMethods.Add(new LifecycleElement(methodInfo, destroyAttribute.Order)); + } + } + + initMethods.InsertRange(0, curInitMethods.OrderBy(e => e.Order)); + destroyMethods.InsertRange(0, curDestroyMethods.OrderBy(e => e.Order)); + instanceType = instanceType.BaseType; + } + while (instanceType != null && instanceType != typeof(Object)); + + var objectDef = _objectFactory.GetObjectDefinition(name); + var metadata = new LifecycleLifecycleMetadata(initMethods, destroyMethods); + metadata.CheckConfigMembers(objectDef); + + return metadata; + } + + + private class LifecycleLifecycleMetadata + { + private readonly IList _initMethods; + private readonly IList _destroyMethods; + + + public LifecycleLifecycleMetadata(IList initMethods, IList destroyMethods) + { + _initMethods = initMethods; + _destroyMethods = destroyMethods; + } + + public void CheckConfigMembers(IObjectDefinition objectDef) + { + lock (_initMethods) + { + for (int i = 0; i < _initMethods.Count; i++) + { + if (!_initMethods[i].CheckConfig(objectDef)) + _initMethods.Remove(_initMethods[i]); + } + } + lock (_destroyMethods) + { + for (int i = 0; i < _destroyMethods.Count; i++) + { + if (!_destroyMethods[i].CheckConfig(objectDef)) + _destroyMethods.Remove(_destroyMethods[i]); + } + } + } + + public void InvokeInitMethods(object instance, string objectName) + { + foreach (var lifecycleElement in _initMethods) + { + lifecycleElement.Invoke(instance, objectName); + } + } + + public void InvokeDestroyMethods(object instance, string objectName) + { + foreach (var lifecycleElement in _destroyMethods) + { + lifecycleElement.Invoke(instance, objectName); + } + } + + } + + + private class LifecycleElement + { + private readonly MethodInfo _method; + private readonly int _order; + + public int Order { get { return _order; } } + + + public LifecycleElement(MethodInfo method, int order) + { + _method = method; + _order = order; + } + + public bool CheckConfig(IObjectDefinition objectDef) + { + if (_method.Name == objectDef.InitMethodName || _method.Name == objectDef.DestroyMethodName) + return false; + + return true; + } + + public void Invoke(object instance, string objectName) + { + Logger.Debug(m => m("Invoking init method on object '" + objectName + "': " + _method.Name)); + _method.Invoke(instance, new object[] {}); + } + } + + } +} diff --git a/src/Spring/Spring.Core/Objects/Factory/Attributes/PostConstructAttribute.cs b/src/Spring/Spring.Core/Objects/Factory/Attributes/PostConstructAttribute.cs new file mode 100644 index 00000000..16cf0dd6 --- /dev/null +++ b/src/Spring/Spring.Core/Objects/Factory/Attributes/PostConstructAttribute.cs @@ -0,0 +1,62 @@ +#region License + +/* + * Copyright © 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + + +using System; + +namespace Spring.Objects.Factory.Attributes +{ + /// + /// Defines a method that will be called during the intantiation of an instance + /// + [AttributeUsage(AttributeTargets.Method)] + public class PostConstructAttribute : Attribute + { + private int _order; + + + /// + /// Initializes a new instance of the PostConstruct class with order = 1 + /// + public PostConstructAttribute() + { + _order = int.MaxValue; + } + + /// + /// Initializes a new instance of the PostConstruct class with defined order + /// + /// Order in which the PostContruct method is called + public PostConstructAttribute(int order) + { + _order = order; + } + + + /// + /// Defined the order in which the PostContruct methods are called + /// + public int Order + { + get { return _order; } + set { _order = value; } + } + } +} diff --git a/src/Spring/Spring.Core/Objects/Factory/Attributes/PreDestroyAttribute.cs b/src/Spring/Spring.Core/Objects/Factory/Attributes/PreDestroyAttribute.cs new file mode 100644 index 00000000..78e52fb7 --- /dev/null +++ b/src/Spring/Spring.Core/Objects/Factory/Attributes/PreDestroyAttribute.cs @@ -0,0 +1,61 @@ +#region License + +/* + * Copyright © 2010-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; + +namespace Spring.Objects.Factory.Attributes +{ + /// + /// Defines a method that will be called prior to the destruction of the object instance + /// + [AttributeUsage(AttributeTargets.Method)] + public class PreDestroyAttribute : Attribute + { + private int _order; + + + /// + /// Initializes a new instance of the PreDestroy attribute with order = 1 + /// + public PreDestroyAttribute() + { + _order = int.MaxValue; + } + + /// + /// Initializes a new instance of the PreDestroy attribute with defined order + /// + /// Order in which the PostContruct method is called + public PreDestroyAttribute(int order) + { + _order = order; + } + + + /// + /// Defined the order in which the PreDestroy methods are called + /// + public int Order + { + get { return _order; } + set { _order = value; } + } + } +} diff --git a/src/Spring/Spring.Core/Spring.Core.2008.csproj b/src/Spring/Spring.Core/Spring.Core.2008.csproj index 2dcd89a4..cce84032 100644 --- a/src/Spring/Spring.Core/Spring.Core.2008.csproj +++ b/src/Spring/Spring.Core/Spring.Core.2008.csproj @@ -673,6 +673,9 @@ Code + + + diff --git a/src/Spring/Spring.Core/Spring.Core.2010.csproj b/src/Spring/Spring.Core/Spring.Core.2010.csproj index e6f1052e..99022b87 100644 --- a/src/Spring/Spring.Core/Spring.Core.2010.csproj +++ b/src/Spring/Spring.Core/Spring.Core.2010.csproj @@ -675,6 +675,9 @@ Code + + + diff --git a/test/Spring/Spring.Core.Tests/Objects/Factory/Attributes/PostConstructAttributeTests.cs b/test/Spring/Spring.Core.Tests/Objects/Factory/Attributes/PostConstructAttributeTests.cs new file mode 100644 index 00000000..64058fd7 --- /dev/null +++ b/test/Spring/Spring.Core.Tests/Objects/Factory/Attributes/PostConstructAttributeTests.cs @@ -0,0 +1,178 @@ +#region License + +/* + * Copyright © 2002-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using NUnit.Framework; +using Spring.Context.Support; +using Spring.Objects.Factory.Config; +using Spring.Objects.Factory.Support; + +namespace Spring.Objects.Factory.Attributes +{ + [TestFixture] + public class PostConstructAttributeTests + { + private GenericApplicationContext _applicationContext; + + + [SetUp] + public void Setup() + { + _applicationContext = new GenericApplicationContext(); + + var objDef = new RootObjectDefinition(typeof (InitDestroyAttributeObjectPostProcessor)); + objDef.Role = ObjectRole.ROLE_INFRASTRUCTURE; + _applicationContext.ObjectFactory.RegisterObjectDefinition("InitDestroyAttributeObjectPostProcessor", objDef); + + objDef = new RootObjectDefinition(typeof(PostContructTestObject1)); + _applicationContext.ObjectFactory.RegisterObjectDefinition("PostContructTestObject1", objDef); + + objDef = new RootObjectDefinition(typeof(PostContructTestObject2)); + _applicationContext.ObjectFactory.RegisterObjectDefinition("PostContructTestObject2", objDef); + + objDef = new RootObjectDefinition(typeof(PostContructTestObject3)); + _applicationContext.ObjectFactory.RegisterObjectDefinition("PostContructTestObject3", objDef); + + objDef = new RootObjectDefinition(typeof(PostContructTestObject4)); + objDef.Scope = "prototype"; + _applicationContext.ObjectFactory.RegisterObjectDefinition("PostContructTestObject4", objDef); + + objDef = new RootObjectDefinition(typeof(PostContructTestObject5)); + _applicationContext.ObjectFactory.RegisterObjectDefinition("PostContructTestObject5", objDef); + + objDef = new RootObjectDefinition(typeof(PostContructTestObject1)); + objDef.InitMethodName = "Init1"; + _applicationContext.ObjectFactory.RegisterObjectDefinition("PostContructTestObject6", objDef); + + _applicationContext.Refresh(); + } + + + [Test] + public void PostContructMethodExecuted() + { + var testObj = (PostContructTestObject1)_applicationContext.GetObject("PostContructTestObject1"); + + Assert.That(testObj.InitCalled, Is.EqualTo(1)); + } + + [Test] + public void ExecutedInCorrectOrder() + { + var testObj = (PostContructTestObject2)_applicationContext.GetObject("PostContructTestObject2"); + + Assert.That(testObj.InitCalled, Is.EqualTo(2), "Two PostContruct methods defined, need to have two method calls."); + Assert.That(testObj.CalledAfter, Is.True, "Order of PostConstruct not followed."); + } + + [Test] + public void NoExceptionIfMethodHasReturnValue() + { + var testObj = (PostContructTestObject3)_applicationContext.GetObject("PostContructTestObject3"); + + Assert.That(testObj.InitCalled, Is.EqualTo(1), "A PostContruct method with return value should run the init method."); + } + + [Test] + public void WithArgumentMustThrowException() + { + Assert.That(delegate { _applicationContext.GetObject("PostContructTestObject4"); }, Throws.Exception.TypeOf()); + } + + [Test] + public void AttributeOnbaseTypeMethod() + { + var testObj = (PostContructTestObject5)_applicationContext.GetObject("PostContructTestObject5"); + + Assert.That(testObj.InitCalled, Is.EqualTo(1)); + } + + [Test] + public void SameMethodDefinedInXml() + { + var testObj = (PostContructTestObject1)_applicationContext.GetObject("PostContructTestObject6"); + + Assert.That(testObj.InitCalled, Is.EqualTo(1)); + } + } + + + public class PostContructTestObject1 + { + public int InitCalled { get; set; } + + [PostConstruct] + public void Init1() + { + InitCalled++; + } + } + + public class PostContructTestObject2 + { + public int InitCalled { get; set; } + public bool Init1Called { get; set; } + public bool CalledAfter { get; set; } + + [PostConstruct(Order = 1)] + public void Init1() + { + InitCalled++; + Init1Called = true; + } + + [PostConstruct(Order = 2)] + public void Init2() + { + InitCalled++; + if (Init1Called) + CalledAfter = true; + } + } + + public class PostContructTestObject3 + { + public int InitCalled { get; set; } + + [PostConstruct] + private bool Init1() + { + InitCalled++; + return true; + } + } + + public class PostContructTestObject4 + { + public int InitCalled { get; set; } + + [PostConstruct] + private void Init1(bool enabled) + { + InitCalled++; + } + } + + public class PostContructTestObject5 : PostContructTestObject1 + { + + } + + +} diff --git a/test/Spring/Spring.Core.Tests/Objects/Factory/Attributes/PreDestroyAttributeTests.cs b/test/Spring/Spring.Core.Tests/Objects/Factory/Attributes/PreDestroyAttributeTests.cs new file mode 100644 index 00000000..436710c9 --- /dev/null +++ b/test/Spring/Spring.Core.Tests/Objects/Factory/Attributes/PreDestroyAttributeTests.cs @@ -0,0 +1,192 @@ +#region License + +/* + * Copyright © 2002-2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using NUnit.Framework; +using Spring.Context.Support; +using Spring.Objects.Factory.Config; +using Spring.Objects.Factory.Support; + +namespace Spring.Objects.Factory.Attributes +{ + [TestFixture] + public class PreDestroyAttributeTests + { + private GenericApplicationContext _applicationContext; + + + [SetUp] + public void Setup() + { + _applicationContext = new GenericApplicationContext(); + + var objDef = new RootObjectDefinition(typeof(InitDestroyAttributeObjectPostProcessor)); + objDef.Role = ObjectRole.ROLE_INFRASTRUCTURE; + _applicationContext.ObjectFactory.RegisterObjectDefinition("InitDestroyAttributeObjectPostProcessor", objDef); + + objDef = new RootObjectDefinition(typeof(PreDestroyTestObject1)); + _applicationContext.ObjectFactory.RegisterObjectDefinition("PreDestroyTestObject1", objDef); + + objDef = new RootObjectDefinition(typeof(PreDestroyTestObject2)); + _applicationContext.ObjectFactory.RegisterObjectDefinition("PreDestroyTestObject2", objDef); + + objDef = new RootObjectDefinition(typeof(PreDestroyTestObject3)); + objDef.DestroyMethodName = "Destroy"; + _applicationContext.ObjectFactory.RegisterObjectDefinition("PreDestroyTestObject3", objDef); + + objDef = new RootObjectDefinition(typeof(PreDestroyTestObject4)); + objDef.Scope = "prototype"; + _applicationContext.ObjectFactory.RegisterObjectDefinition("PreDestroyTestObject4", objDef); + + objDef = new RootObjectDefinition(typeof(PreDestroyTestObject5)); + _applicationContext.ObjectFactory.RegisterObjectDefinition("PreDestroyTestObject5", objDef); + + _applicationContext.Refresh(); + } + + [Test] + public void PreDestroyMethodExecution() + { + DestroyTester.ExecutionCount1 = 0; + var testObj = _applicationContext.GetObject("PreDestroyTestObject1"); + _applicationContext.Dispose(); + + Assert.That(DestroyTester.ExecutionCount1, Is.EqualTo(1)); + } + + [Test] + public void PreDestroyInBaseType() + { + DestroyTester.ExecutionCount2 = 0; + var testObj = _applicationContext.GetObject("PreDestroyTestObject2"); + _applicationContext.Dispose(); + + Assert.That(DestroyTester.ExecutionCount2, Is.EqualTo(1)); + } + + [Test] + public void SameMethodDefinedInXml() + { + DestroyTester.ExecutionCount3 = 0; + var testObj = _applicationContext.GetObject("PreDestroyTestObject3"); + _applicationContext.Dispose(); + + Assert.That(DestroyTester.ExecutionCount3, Is.EqualTo(1)); + } + + [Test] + public void InCorrectOrder() + { + DestroyTester.ExecutionCount4 = 0; + var testObj = (PreDestroyTestObject4)_applicationContext.GetObject("PreDestroyTestObject4"); + _applicationContext.Dispose(); + + Assert.That(DestroyTester.ExecutionCount4, Is.EqualTo(2)); + Assert.That(DestroyTester.CorrectOrder, Is.True); + } + + [Test] + public void WithArgumentMustThrowException() + { + Assert.That(delegate + { + DestroyTester.ExecutionCount5 = 0; + var testObj = (PreDestroyTestObject5)_applicationContext.GetObject("PreDestroyTestObject5"); + _applicationContext.Dispose(); + }, + Throws.Nothing); + } + } + + public class PreDestroyTestObject1 + { + [PreDestroy] + public void Destroy() + { + DestroyTester.ExecutionCount1++; + } + } + + public class PreDestroyBase + { + [PreDestroy] + public void Destroy() + { + DestroyTester.ExecutionCount2++; + } + } + + public class PreDestroyTestObject2 : PreDestroyBase + { + } + + public class PreDestroyTestObject3 + { + [PreDestroy] + public void Destroy() + { + DestroyTester.ExecutionCount3++; + } + } + + public class PreDestroyTestObject4 + { + public PreDestroyTestObject4() + { + DestroyTester.Destroy1Called = false; + DestroyTester.CorrectOrder = false; + } + + [PreDestroy(Order = 1)] + public void Destroy1() + { + DestroyTester.ExecutionCount4++; + DestroyTester.Destroy1Called = true; + } + + [PreDestroy(Order = 2)] + public void Destroy2() + { + DestroyTester.ExecutionCount4++; + if (DestroyTester.Destroy1Called) + DestroyTester.CorrectOrder = true; + } + } + + public class PreDestroyTestObject5 + { + [PreDestroy] + public void Destroy(bool arugment) + { + DestroyTester.ExecutionCount5++; + } + } + + + public static class DestroyTester + { + public static int ExecutionCount1 { get; set; } + public static int ExecutionCount2 { get; set; } + public static int ExecutionCount3 { get; set; } + public static int ExecutionCount4 { get; set; } + public static int ExecutionCount5 { get; set; } + public static bool Destroy1Called { get; set; } + public static bool CorrectOrder { get; set; } + } +} diff --git a/test/Spring/Spring.Core.Tests/Spring.Core.Tests.2008.csproj b/test/Spring/Spring.Core.Tests/Spring.Core.Tests.2008.csproj index 0c4a138c..d3d56c29 100644 --- a/test/Spring/Spring.Core.Tests/Spring.Core.Tests.2008.csproj +++ b/test/Spring/Spring.Core.Tests/Spring.Core.Tests.2008.csproj @@ -316,7 +316,9 @@ + + diff --git a/test/Spring/Spring.Core.Tests/Spring.Core.Tests.2010.csproj b/test/Spring/Spring.Core.Tests/Spring.Core.Tests.2010.csproj index 80c70a18..1644476d 100644 --- a/test/Spring/Spring.Core.Tests/Spring.Core.Tests.2010.csproj +++ b/test/Spring/Spring.Core.Tests/Spring.Core.Tests.2010.csproj @@ -318,7 +318,9 @@ + +