Memoize - kswoll/someta GitHub Wiki
This simple example shows how you can add memoization to a method or property. This is similar to Lazy<T>
but more automatic. It also demonstrates the concept of adding custom fields to the containing class, property/method interception, and instance initialization. We'll take this step by step and start with properties:
[AttributeUsage(AttributeTargets.Property)]
public class MemoizeAttribute : Attribute, IStateExtensionPoint, IPropertyGetInterceptor
{
public InjectedField<object> Field { get; set; }
public object GetPropertyValue(PropertyInfo propertyInfo, object instance, Func<object> getter)
{
var currentValue = Field.GetValue(instance);
if (currentValue == null)
{
currentValue = getter();
Field.SetValue(instance, currentValue);
}
return currentValue;
}
}
- Implements
IStateExtensionPoint
:
This marker interface causes the attribute class to be scanned for properties of typeInjectedField<>
and initializes it with an instance that exposes methods to get and set the value of the field for a particular instance. - Implements
IPropertyGetInterceptor
:
This interface exposes theGetPropertyValue
method, which will get called instead of the original property's getter. To get the value originally provided by the getter, thegetter
delegate is provided to you. - The
Field
property allows us to get and set the cached value.
As you can see here, we put all this together in GetPropertyValue
to get the value from the original getter the first time, store it, and return the cached value in subsequent calls.
[Memoize]
public string ExpensiveGetter
{
get
{
// Only gets called once
// do expensive work
return computedValue;
}
}
You may be thinking to yourself, "ah, but wait! this isn't threadsafe." This is true. To make this threadsafe we will introduce a new field to store an object we will lock
around.
[AttributeUsage(AttributeTargets.Property)]
public class MemoizeAttribute : Attribute, IStateExtensionPoint, IPropertyGetInterceptor, IInstanceInitializer
{
public InjectedField<object> Field { get; set; }
public InjectedField<object> Locker { get; set; }
public void Initialize(object instance, MemberInfo member)
{
Locker.SetValue(instance, new object());
}
public object GetPropertyValue(PropertyInfo propertyInfo, object instance, Func<object> getter)
{
lock (Locker.GetValue(instance))
{
var currentValue = Field.GetValue(instance);
if (currentValue == null)
{
currentValue = getter();
Field.SetValue(instance, currentValue);
}
return currentValue;
}
}
}
- Implements
IInstanceInitializer
Exposes the methodInitialize
which gets called when a given instance is constructed. This method gets called at the end of the original constructor(s).
We use the initializer to create a new instance of object
that we will use for locking. We then modify GetPropertyValue
to surround the original body with a lock
statement.
Finally, let's add support for methods (and async methods):
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
public class MemoizeAttribute : Attribute, IPropertyGetInterceptor, IStateExtensionPoint,
IMethodInterceptor, IAsyncMethodInterceptor, IInstanceInitializer
{
public InjectedField<object> Field { get; set; }
public InjectedField<object> Locker { get; set; }
public void Initialize(object instance, MemberInfo member)
{
if (member is MethodInfo methodInfo && typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
Locker.SetValue(instance, new AsyncLock());
else
Locker.SetValue(instance, new object());
}
public object GetPropertyValue(PropertyInfo propertyInfo, object instance, Func<object> getter)
{
lock (Locker.GetValue(instance))
{
var currentValue = Field.GetValue(instance);
if (currentValue == null)
{
currentValue = getter();
Field.SetValue(instance, currentValue);
}
return currentValue;
}
}
public object Invoke(MethodInfo methodInfo, object instance, object[] parameters, Func<object[], object> invoker)
{
lock (Locker.GetValue(instance))
{
var currentValue = Field.GetValue(instance);
if (currentValue == null)
{
currentValue = invoker(parameters);
Field.SetValue(instance, currentValue);
}
return currentValue;
}
}
public async Task<object> InvokeAsync(MethodInfo methodInfo, object instance, object[] arguments, Func<object[], Task<object>> invoker)
{
using (await AsyncLocker.GetValue(instance).LockAsync())
{
var currentValue = Field.GetValue(instance);
if (currentValue == null)
{
currentValue = await invoker(arguments);
Field.SetValue(instance, currentValue);
}
return currentValue;
}
}
}
- Implements
IAsyncMethodInterceptor
so we can await when proceeding to the original implementation - The initializer now creates a new instance of
object
(orAsyncLock
from Nito.AsyncEx in the case of async methods) that we will use for locking.