6

I've got a simple ASP.NET MVC controller. Inside a few action methods, I access a resource which I'll say is expensive.

So I thought, why not make it static. So instead of doing double checked locking I thought I can leverage the use of Lazy<T> in .NET 4.0. Call the expensive service once instead of multiple times.

So, if this is my pseduo code, how can I change it do use Lazy<T>. For this contrite example, I'll use the File System as the expensive resource So with this example, instead of getting all the files from the destination path, every time a request calls that ActionMethod, I was hoping to use Lazy to hold that list of files .. which of course, makes the call the first time only.

Next assumption: don't worry if the content is changed. That's out of scope, here.

public class FooController : Controller { private readonly IFoo _foo; public FooController(IFoo foo) { _foo = foo; } public ActionResult PewPew() { // Grab all the files in a folder. // nb. _foo.PathToFiles = "/Content/Images/Harro" var files = Directory.GetFiles(Server.MapPath(_foo.PathToFiles)); // Note: No, I wouldn't return all the files but a concerete view model // with only the data from a File object, I require. return View(files); } } 
2
  • 1
    What's wrong with using the ASP.NET Cache? Commented Sep 21, 2011 at 2:44
  • 1
    It sounds like you're looking for a singleton, rather than lazy instantiation of an object. Of course, you can use Lazy to create a singleton... Commented Sep 21, 2011 at 2:47

4 Answers 4

5

In your example, the result of Directory.GetFiles depends on the value of _foo, which is not static. Therefore you cannot use a static instance of Lazy<string[]> as a shared cache between all instances of your controller.

The ConcurrentDictionary<TKey, TValue> sounds like something that is closer to what you want.

// Code not tested, blah blah blah... public class FooController : Controller { private static readonly ConcurrentDictionary<string, string[]> _cache = new ConcurrentDictionary<string, string[]>(); private readonly IFoo _foo; public FooController(IFoo foo) { _foo = foo; } public ActionResult PewPew() { var files = _cache.GetOrAdd(Server.MapPath(_foo.PathToFiles), path => { return Directory.GetFiles(path); }); return View(files); } } 
Sign up to request clarification or add additional context in comments.

4 Comments

This is a great idea! With the cache option (given by Martin or Chris), it's possible that more than one request will try to insert into the cache -if- the requests are happening (more or less) at the same time .. right? (race condition sorta thing) while the concurrent dictionary prevents the 2nd, 3rd request (at the same time) from executing the expensive resource, right?
@Pure - Actually no, the ConcurrentDictionary may invoke the "valueFactory" function more than once if multiple threads try to get the value before it has been added to the cache. The ConcurrentDictionary is thread safe (only one value will be added for each key), but the invocation of valueFactory doesn't occur inside a lock.
The ConcurrentDictionary is thread safe however the delegate passed to GetOrAdd and AddOrUpdate is invoked outside of the dictionary's internal lock. More here: msdn.microsoft.com/en-us/library/dd997369.aspx
So if you just create a Lazy<T> in your factory method, you can avoid executing the expensive operation multiple times.
4

I agree with Greg that Lazy<> is inappropriate here.

You could try using asp.net caching to cache the contents of a folder, using _foo.PathToFiles as your key. This has an advantage over Lazy<> that you can control the lifetime of the caching so it will refetch the contents say every day or every week without requiring an application restart.

Also caching is friendly to your server in that it will gracefully degrade if there is not enough memory to support it.

2 Comments

+1 for recommending the built-in ASP.NET caching. Don't reinvent the wheel until you have a solid reason to do so.
Yes thanks. I like your inline cache too - maybe not for this scenario but could be useful for windows forms apps.
2

Lazy<T> works best when you're not sure if you're going to need the resource, so it's loaded just-in-time only when it's actually needed. The action is always going to load the resource regardless, but because it's expensive you probably want to cache it somewhere? You could try something like this:

public ActionResult PewPew() { MyModel model; const string cacheKey = "resource"; lock (controllerLock) { if (HttpRuntime.Cache[cacheKey] == null) { HttpRuntime.Cache.Insert(cacheKey, LoadExpensiveResource()); } model = (MyModel) HttpRuntime.Cache[cacheKey]; } return View(model); } 

Comments

1

I just had the same problem you described so I created a class CachedLazy<T> -> allows values to be shared between controller instances, but with optional timed expiry and one-time creation unlike ConcurrentDictionary.

/// <summary> /// Provides a lazily initialised and HttpRuntime.Cache cached value. /// </summary> public class CachedLazy<T> { private readonly Func<T> creator; /// <summary> /// Key value used to store the created value in HttpRuntime.Cache /// </summary> public string Key { get; private set; } /// <summary> /// Optional time span for expiration of the created value in HttpRuntime.Cache /// </summary> public TimeSpan? Expiry { get; private set; } /// <summary> /// Gets the lazily initialized or cached value of the current Cached instance. /// </summary> public T Value { get { var cache = HttpRuntime.Cache; var value = cache[Key]; if (value == null) { lock (cache) { // After acquiring lock, re-check that the value hasn't been created by another thread value = cache[Key]; if (value == null) { value = creator(); if (Expiry.HasValue) cache.Insert(Key, value, null, Cache.NoAbsoluteExpiration, Expiry.Value); else cache.Insert(Key, value); } } } return (T)value; } } /// <summary> /// Initializes a new instance of the CachedLazy class. If lazy initialization occurs, the given /// function is used to get the value, which is then cached in the HttpRuntime.Cache for the /// given time span. /// </summary> public CachedLazy(string key, Func<T> creator, TimeSpan? expiry = null) { this.Key = key; this.creator = creator; this.Expiry = expiry; } } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.