Approaches for generic, compile-time safe lazy-load methods
- by Aaronaught
Suppose I have created a wrapper class like the following:
public class Foo : IFoo
{
private readonly IFoo innerFoo;
public Foo(IFoo innerFoo)
{
this.innerFoo = innerFoo;
}
public int? Bar { get; set; }
public int? Baz { get; set; }
}
The idea here is that the innerFoo might wrap data-access methods or something similarly expensive, and I only want its GetBar and GetBaz methods to be invoked once. So I want to create another wrapper around it, which will save the values obtained on the first run.
It's simple enough to do this, of course:
int IFoo.GetBar()
{
if ((Bar == null) && (innerFoo != null))
Bar = innerFoo.GetBar();
return Bar ?? 0;
}
int IFoo.GetBaz()
{
if ((Baz == null) && (innerFoo != null))
Baz = innerFoo.GetBaz();
return Baz ?? 0;
}
But it gets pretty repetitive if I'm doing this with 10 different properties and 30 different wrappers. So I figured, hey, let's make this generic:
T LazyLoad<T>(ref T prop, Func<IFoo, T> loader)
{
if ((prop == null) && (innerFoo != null))
prop = loader(innerFoo);
return prop;
}
Which almost gets me where I want, but not quite, because you can't ref an auto-property (or any property at all). In other words, I can't write this:
int IFoo.GetBar()
{
return LazyLoad(ref Bar, f => f.GetBar()); // <--- Won't compile
}
Instead, I'd have to change Bar to have an explicit backing field and write explicit getters and setters. Which is fine, except for the fact that I end up writing even more redundant code than I was writing in the first place.
Then I considered the possibility of using expression trees:
T LazyLoad<T>(Expression<Func<T>> propExpr, Func<IFoo, T> loader)
{
var memberExpression = propExpr.Body as MemberExpression;
if (memberExpression != null)
{
// Use Reflection to inspect/set the property
}
}
This plays nice with refactoring - it'll work great if I do this:
return LazyLoad(f => f.Bar, f => f.GetBar());
But it's not actually safe, because someone less clever (i.e. myself in 3 days from now when I inevitably forget how this is implemented internally) could decide to write this instead:
return LazyLoad(f => 3, f => f.GetBar());
Which is either going to crash or result in unexpected/undefined behaviour, depending on how defensively I write the LazyLoad method. So I don't really like this approach either, because it leads to the possibility of runtime errors which would have been prevented in the first attempt. It also relies on Reflection, which feels a little dirty here, even though this code is admittedly not performance-sensitive.
Now I could also decide to go all-out and use DynamicProxy to do method interception and not have to write any code, and in fact I already do this in some applications. But this code is residing in a core library which many other assemblies depend on, and it seems horribly wrong to be introducing this kind of complexity at such a low level. Separating the interceptor-based implementation from the IFoo interface by putting it into its own assembly doesn't really help; the fact is that this very class is still going to be used all over the place, must be used, so this isn't one of those problems that could be trivially solved with a little DI magic.
The last option I've already thought of would be to have a method like:
T LazyLoad<T>(Func<T> getter, Action<T> setter, Func<IFoo, T> loader) { ... }
This option is very "meh" as well - it avoids Reflection but is still error-prone, and it doesn't really reduce the repetition that much. It's almost as bad as having to write explicit getters and setters for each property.
Maybe I'm just being incredibly nit-picky, but this application is still in its early stages, and it's going to grow substantially over time, and I really want to keep the code squeaky-clean.
Bottom line: I'm at an impasse, looking for other ideas.
Question:
Is there any way to clean up the lazy-loading code at the top, such that the implementation will:
Guarantee compile-time safety, like the ref version;
Actually reduce the amount of code repetition, like the Expression version; and
Not take on any significant additional dependencies?
In other words, is there a way to do this just using regular C# language features and possibly a few small helper classes? Or am I just going to have to accept that there's a trade-off here and strike one of the above requirements from the list?