diff --git a/src/Spring/Spring.Aop/Aspects/Cache/CacheResultAdvice.cs b/src/Spring/Spring.Aop/Aspects/Cache/CacheResultAdvice.cs index a6883095..efe272a5 100644 --- a/src/Spring/Spring.Aop/Aspects/Cache/CacheResultAdvice.cs +++ b/src/Spring/Spring.Aop/Aspects/Cache/CacheResultAdvice.cs @@ -1,19 +1,19 @@ #region License -/* - * Copyright © 2002-2006 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. +/* + * Copyright © 2002-2006 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 @@ -84,8 +84,26 @@ namespace Spring.Aspects.Cache #endregion + /// + /// Inner class to help cache null values. + /// + [Serializable] public sealed class NullValueMarkerType + { + /// true when other object is of same type. + public override bool Equals(object obj) + { + return obj is NullValueMarkerType; + } + + /// 13 + public override int GetHashCode() + { + return 13; + } + } + // NullValue - private static readonly object NullValue = new object(); + private static readonly object NullValue = new NullValueMarkerType(); /// /// Applies caching around a method invocation. @@ -170,6 +188,26 @@ namespace Spring.Aspects.Cache ICache cache = GetCache(resultInfo.CacheName); returnValue = cache.Get(resultKey); cacheHit = (returnValue != null); + + if (NullValue.Equals(returnValue)) + { + returnValue = null; + } + + Type returnType = invocation.Method.ReturnType; + if (returnValue != null && !returnType.IsInstanceOfType(returnValue)) + { + #region Instrumentation + if (isLogDebugEnabled) + { + logger.Debug(String.Format("Object for key [{0}] was of type [{1}] which is not compatible with return type [{2}]. Proceeding...", resultKey, returnValue.GetType(), returnType)); + } + #endregion + + cacheHit = false; + returnValue = null; + } + if (!cacheHit) { #region Instrumentation @@ -201,7 +239,7 @@ namespace Spring.Aspects.Cache #endregion } - return (returnValue == NullValue) ? null : returnValue; + return returnValue; } cacheHit = false; diff --git a/test/Spring/Spring.Aop.Tests/Aspects/Cache/CacheAspectIntegrationTests.cs b/test/Spring/Spring.Aop.Tests/Aspects/Cache/CacheAspectIntegrationTests.cs index 188751fd..31a38122 100644 --- a/test/Spring/Spring.Aop.Tests/Aspects/Cache/CacheAspectIntegrationTests.cs +++ b/test/Spring/Spring.Aop.Tests/Aspects/Cache/CacheAspectIntegrationTests.cs @@ -62,14 +62,14 @@ namespace Spring.Aspects.Cache ProxyFactory pf = new ProxyFactory(new InventorStore()); pf.AddAdvisors(cacheAspect); - IInventorStore store = (IInventorStore) pf.GetProxy(); + IInventorStore store = (IInventorStore)pf.GetProxy(); Assert.AreEqual(0, cache.Count); - + IList inventors = store.GetAll(); Assert.AreEqual(2, cache.Count); - store.Delete((Inventor) inventors[0]); + store.Delete((Inventor)inventors[0]); Assert.AreEqual(1, cache.Count); Inventor tesla = store.Load("Nikola Tesla"); @@ -83,6 +83,24 @@ namespace Spring.Aspects.Cache Assert.AreEqual(0, cache.Count); } + [Test] + public void TestCacheResultDoesNotReturnInvalidType() + { + ICache cache = new NonExpiringCache(); + context.ObjectFactory.RegisterSingleton("inventors", cache); + + ProxyFactory pf = new ProxyFactory(new InventorStore()); + pf.AddAdvisors(cacheAspect); + + IInventorStore store = (IInventorStore)pf.GetProxy(); + + cache.Insert("Nikola Tesla", "WrongTypeForMethodSignature"); + + Inventor value = store.Load("Nikola Tesla"); + + Assert.That(value, Is.AssignableTo(typeof(Inventor)), "CacheAspect returned a cached type that is incompatible with the method signature return type"); + } + /// /// http://jira.springframework.org/browse/SPRNET-1226 /// diff --git a/test/Spring/Spring.Aop.Tests/Aspects/Cache/CacheResultAdviceTests.cs b/test/Spring/Spring.Aop.Tests/Aspects/Cache/CacheResultAdviceTests.cs index c0277455..30e32113 100644 --- a/test/Spring/Spring.Aop.Tests/Aspects/Cache/CacheResultAdviceTests.cs +++ b/test/Spring/Spring.Aop.Tests/Aspects/Cache/CacheResultAdviceTests.cs @@ -21,7 +21,9 @@ #region Imports using System.Collections; +using System.IO; using System.Reflection; +using System.Runtime.Serialization.Formatters.Binary; using AopAlliance.Intercept; using DotNetMock.Dynamic; using NUnit.Framework; @@ -46,6 +48,7 @@ namespace Spring.Aspects.Cache private CacheResultAdvice advice; private ICache resultCache; private ICache itemCache; + private ICache binaryFormatterCache; private CacheResultTarget cacheResultTarget = new CacheResultTarget(); [SetUp] @@ -59,6 +62,7 @@ namespace Spring.Aspects.Cache resultCache = new NonExpiringCache(); itemCache = new NonExpiringCache(); + binaryFormatterCache = new BinaryFormatterCache(); } /// @@ -86,6 +90,36 @@ namespace Spring.Aspects.Cache mockContext.Verify(); } + [Test] + public void CacheResultOfMethodThatReturnsNullWithSerializingCache() + { + MethodInfo method = new VoidMethod(cacheResultTarget.ReturnsNothing).Method; + object expectedReturnValue = null; + + ExpectAttributeRetrieval(method); + ExpectCacheKeyGeneration(method, null); + ExpectCacheInstanceRetrieval("results", binaryFormatterCache); + ExpectCallToProceed(expectedReturnValue); + + // check that the null retVal is cached as well - it might be + // the result of an expensive webservice/database call etc. + object returnValue = advice.Invoke((IMethodInvocation) mockInvocation.Object); + Assert.AreEqual(expectedReturnValue, returnValue); + Assert.AreEqual(1, binaryFormatterCache.Count); + + // and again, but without Proceed()... + ExpectAttributeRetrieval(method); + ExpectCacheKeyGeneration(method, null); + ExpectCacheInstanceRetrieval("results", binaryFormatterCache); + + // cached value should be returned + object cachedValue = advice.Invoke((IMethodInvocation)mockInvocation.Object); + Assert.IsNull(cachedValue, "Should recognize cached value as null-value marker."); + + mockInvocation.Verify(); + mockContext.Verify(); + } + [Test] public void CacheResultOfMethodThatReturnsObject() { @@ -571,4 +605,33 @@ namespace Spring.Aspects.Cache } #endregion + + class BinaryFormatterCache : NonExpiringCache + { + protected override void DoInsert(object key, object value, System.TimeSpan timeToLive) + { + BinaryFormatter fmt = new BinaryFormatter(); + using (MemoryStream stream = new MemoryStream()) + { + fmt.Serialize(stream, value); + stream.Seek(0, SeekOrigin.Begin); + value = stream.ToArray(); + } + + base.DoInsert(key, value, timeToLive); + } + + public override object Get(object key) + { + byte[] bytes = (byte[]) base.Get(key); + + if (bytes == null) + { + return null; + } + + BinaryFormatter fmt = new BinaryFormatter(); + return fmt.Deserialize(new MemoryStream(bytes)); + } + } }