2
\$\begingroup\$

I wanted to cache methods using unity interception and not have the parameters passed in stay alive for the sake of caching. If the objects would have gone out of scope without caching then the cache should be invalidated. Also wanted to use MemoryCache class as if memory is low the cache should be the first thing to go. My work place rules prevents me from using a 3rd party library.

I'm using the ConditionalWeakTable to hold on to the target (again don't want to keep target alive for caching) Each table contains a dictionary that will have a key to link into the MemoryCache class.

public class CacheMethodHandler : ICallHandler
{
    private readonly TimeSpan _offSet;

    public CacheMethodHandler(TimeSpan offSet)
    {
        _offSet = offSet;
    }

    private static readonly MemoryCache MemCache = new MemoryCache("CacheMethodHandler");

    private static readonly ConditionalWeakTable<object, ConcurrentDictionary<CacheMethodSink, Guid>>
        CacheManager =
            new ConditionalWeakTable<object, ConcurrentDictionary<CacheMethodSink, Guid>>();

    public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
    {
        var cacheManager = CacheManager.GetOrCreateValue(input.Target);

        // clean up dead keys
        RemoveDeadKeys(cacheManager);

        // add our key to the cachemanager
        //   the add function could be called mutliple times but it's ok as we will have the ID of the last one
        var sink = new CacheMethodSink(input);
        var id = cacheManager.GetOrAdd(sink, s =>
        {
            var result = Guid.NewGuid();
            s.Id = result;
            return result;
        });

        // wrap in lazy call as it serves two purposes
        //  1.  we don't execute it unless we need it
        //  2.  it does thread safety for us
        var cacheData = new Lazy<IMethodReturn>(() =>
        {
            // need to create a new IMethodReturn as one coming back holds reference to parameters passed in
            var methodReturn = getNext()(input, getNext);
            if (methodReturn.Exception != null)
            {
                return new CacheMethodReturn(input, methodReturn.Exception);
            }

            return new CacheMethodReturn(input, methodReturn.ReturnValue,
                methodReturn.Outputs.Cast<object>().ToArray());
        }, true);

        // add the lazy item or get back the one previous stored
        var storedData =
            (Lazy<IMethodReturn>)
                MemCache.AddOrGetExisting(id.ToString("D"), cacheData, DateTimeOffset.Now.Add(_offSet));

        // if storeddata is null then we need to use the cacheData we created
        if (storedData != null)
        {
            cacheData = storedData;
        }

        var results = cacheData.Value;

        return results;
    }

    public int Order { get; set; }

    /// <summary>
    /// Removes dead keys from the dictionary
    /// </summary>
    /// <param name="cacheManager"></param>
    private void RemoveDeadKeys(ConcurrentDictionary<CacheMethodSink, Guid> cacheManager)
    {
        foreach (var deadKeys in cacheManager.Keys.Where(s => s.IsDead).ToArray())
        {
            Guid value;
            cacheManager.TryRemove(deadKeys, out value);
        }
    }

    private class CacheMethodSink
    {
        private readonly object[] _parameters;
        private readonly MethodBase _methodBase;

        /// <summary>
        ///  Simple property to know if any WeakReference are no longer alive
        /// </summary>
        public bool IsDead
        {
            get
            {
                _isDead = _isDead || _parameters.OfType<WeakReference>().Any(w => !w.IsAlive);
                return _isDead;
            }
        }

        private bool _isDead;

        /// <summary>
        /// Id to use to remove from memory cache
        /// </summary>
        public Guid Id { private get; set; }

        public CacheMethodSink(IMethodInvocation input)
        {
            _parameters = ConvertParameters(input).ToArray();
            _methodBase = input.MethodBase;
        }

        /// <summary>
        /// Converts parameters into weakreferences for non value types and expands out params array
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        private IEnumerable<object> ConvertParameters(IMethodInvocation input)
        {
            return input.MethodBase.GetParameters().Select(p => new 
            {
                Skip = p.GetCustomAttribute<CacheIngnoreAttribute>() != null || p.IsOut,
                ParamArray = p.GetCustomAttribute<ParamArrayAttribute>() != null,
                Value = input.Arguments[p.Name]
            }).SelectMany(a =>
            {
                if (a.Skip)
                {
                    return new object[1];
                }

                if (a.ParamArray)
                {
                    var array =  a.Value as Array;
                    if (array != null && array.Rank == 1)
                    {
                        return array.OfType<object>();
                    }
                }

                return new[] {a.Value};
            }).Select(WrapParameter);
        }


        private object WrapParameter(object value)
        {
            if (value == null || value.GetType().IsValueType || value is string)
            {
                // value type then just push it back out
                return value;
            }
            else
            {
                // wrap parameters in WeakReference so we don't keep objects alive
                return new WeakReference(value);
            }
        }

        // unwraps any objects from a weak refernce 
        private object UnWrapParameter(object value, CacheMethodSink sink)
        {
            if (value == null || sink._isDead)
            {
                return null;
            }
            else if (value.GetType().IsValueType || value is string)
            {
                return value;
            }
            else
            {
                var weakRef = (WeakReference) value;
                var target = weakRef.Target;
                if (weakRef.IsAlive)
                {
                    return target;
                }
                else
                {
                    // mark as dead so next time will get cleaned up
                    if (!sink._isDead)
                    {
                        sink._isDead = true;
                    }
                    return null;
                }
            }
        }

        public override int GetHashCode()
        {
            return Tuple.Create(_methodBase, _parameters.Length).GetHashCode();
        }

        public override bool Equals(object obj)
        {
            var value = obj as CacheMethodSink;
            if (value == null)
            {
                return false;
            }
            if (value._methodBase != this._methodBase)
            {
                return false;
            }
            if (value._isDead != _isDead)
            {
                return false;
            }

            return
                _parameters.Select(o => UnWrapParameter(o, this))
                    .SequenceEqual(value._parameters.Select(o => UnWrapParameter(o, value)));
        }

        ~CacheMethodSink()
        {
            // if memcache still has value then clean it up
            MemCache.Remove(Id.ToString("D"));
        }
    }
}

Here is the CacheAttribute

[AttributeUsage(AttributeTargets.Method)]
public class CacheAttribute : HandlerAttribute
{

    public int Days { get; set; }
    public int Hours { get; set; }
    public int Minutes { get; set; }
    public int Seconds { get; set; }
    public int Milliseconds { get; set; }

    public override ICallHandler CreateHandler(IUnityContainer container)
    {
        return
            container.Resolve<CacheMethodHandler>(
                new DependencyOverride<TimeSpan>(new TimeSpan(Days, Hours, Minutes, Seconds, Milliseconds)));
    }
}

Simple attribute to know if I should ignore a parameter

[AttributeUsage(AttributeTargets.Parameter)]
public class CacheIngnoreAttribute : Attribute
{
}

Then IMethodReturn class I created because Unity's default ones would hold reference to parameters passed in

public class CacheMethodReturn : IMethodReturn
{
    public CacheMethodReturn(IMethodInvocation originalInvocation, Exception exception)
    {
        InvocationContext = originalInvocation.InvocationContext;
        Exception = exception;
        Outputs = new ParameterCollection(new object[0], new ParameterInfo[0], _ => false);
    }

    public CacheMethodReturn(IMethodInvocation originalInvocation, object returnValue, object[] arguments)
    {
        InvocationContext = originalInvocation.InvocationContext;
        ReturnValue = returnValue;
        Outputs = new ParameterCollection(arguments,
            originalInvocation.MethodBase.GetParameters().Where(pi => pi.ParameterType.IsByRef).ToArray(), _ => true);
    }

    public IParameterCollection Outputs { get; }
    public object ReturnValue { get; set; }
    public Exception Exception { get; set; }
    public IDictionary<string, object> InvocationContext { get; }
}

Usages would be as follows

public class CacheTest
{
    [Cache(Seconds = 30)]
    public virtual IEnumerable<Tuple<int, DateTime>> NormalTest(int replicate, params int[] testdata)
    {
        return Enumerable.Range(0, replicate).Select(i => Tuple.Create(i, DateTime.Now)).ToArray();
    }

    [Cache(Seconds = 30)]
    public virtual async Task<IEnumerable<Tuple<string, DateTime>>> AsyncTest(int wait, [CacheIngnore] CancellationToken token)
    {
        await Task.Delay(wait, token);
        if (token.IsCancellationRequested)
        {
            return Enumerable.Empty<Tuple<string, DateTime>>();
        }
        return Enumerable.Range(0, wait).Select(n => Tuple.Create(n.ToString(), DateTime.Now)).ToArray();
    }

    [Cache(Seconds = 30)]
    public virtual IEnumerable<int> TestOut(int wait, out string moreData)
    {
        moreData = DateTime.Now.ToString();
        return Enumerable.Range(0, wait).ToArray();
    }
}

then to configure the container

var container = new UnityContainer();
container.AddNewExtension<Interception>();
container.RegisterType<CacheTest>(
    new Interceptor<VirtualMethodInterceptor>(),
    new InterceptionBehavior<PolicyInjectionBehavior>());
var test = container.Resolve<CacheTest>();

I'm not exactly happy with having to cycle though to remove dead keys in the CallMethodHandler but can't think of a better way. Also the sink of wrap and unwraping objects and having a finalizer work but looking to see if better options.

\$\endgroup\$
  • \$\begingroup\$ Side note I don't have the rep to create the unity-interception tag as that tag exists in SO \$\endgroup\$ – CharlesNRice Sep 30 '16 at 14:26
  • \$\begingroup\$ Not every tag that exists on SO should also exist here. \$\endgroup\$ – svick Sep 30 '16 at 14:43
  • \$\begingroup\$ @svick I understand that but this question is about unity interception \$\endgroup\$ – CharlesNRice Sep 30 '16 at 14:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.