14

I want to implement an expiration time on a Lazy object. The expiration cooldown must start with the first retrieve of the value. If we get the value, and the expiration time is passed, then we reexecute the function and reset expiration time.

I'm not familiar with extensions, partial keyword, and I don't know the best way to do that.

Thanks

EDIT :

The code so far :

NEW EDIT :

the new code :

public class LazyWithExpiration<T>
{
    private volatile bool expired;
    private TimeSpan expirationTime;
    private Func<T> func;
    private Lazy<T> lazyObject;

    public LazyWithExpiration( Func<T> func, TimeSpan expirationTime )
    {
        this.expirationTime = expirationTime;
        this.func = func;

        Reset();
    }

    public void Reset()
    {
        lazyObject = new Lazy<T>( func );
        expired = false;
    }

    public T Value
    {
        get
        {
            if ( expired )
                Reset();

            if ( !lazyObject.IsValueCreated )
            {
                Task.Factory.StartNew( () =>
                {
                    Thread.Sleep( expirationTime );
                    expired = true;
                } );
            }

            return lazyObject.Value;
        }
    }

}
Bastiflew
  • 1,136
  • 3
  • 18
  • 31
  • 1
    Please show the code you currently have for Lazy loading. SO is not a place where we write all the code for you... show what you have tried so far. – Hogan Sep 08 '13 at 15:01
  • 1
    Extensions and partial keyword I don't think are going to help you here. I would just make a wrapper class for the Lazy (though there is probably a more elegant solution). – Omada Sep 08 '13 at 15:02
  • I agree this is a simple problem, just wrap the accessor with code that checks to see if the timer has expired. I don't think Lazy is needed, but that would change depending on context so please show the code so far. – Hogan Sep 08 '13 at 15:04
  • Why do you need it anyway? Just interested in the reason! – Felix K. Sep 08 '13 at 15:14
  • Could think of many things like a simple cache solution. E.g. reretrieve some RSS feed by recreating the handling object if some time has passed. – Mario Sep 08 '13 at 15:15
  • Sounds like you want caching. – leppie Sep 08 '13 at 15:16
  • You're trying to force some new behavior into the `Lazy` class, which won't work (you'd have to modify it's base behavior). Better create your own wrapper, then use that one inside `Lazy` if you need delayed initialization. If you're using the one I used in my answer (or a similar approach) you won't need `Lazy` at all, because it will already mimic its behavior. – Mario Sep 08 '13 at 15:19
  • @Mario: I think you're right, I better create my own wrapper with a Lazy inside. – Bastiflew Sep 08 '13 at 15:24
  • Based on your approach you won't need `Lazy` at all, e.g. if you create the real object just-in-time you've already got `Lazy` in your own code. – Mario Sep 08 '13 at 15:25
  • From your code it is not clear what you want to achieve. Perhaps you can elaborate a bit what you want to do. – Mare Infinitus Sep 08 '13 at 15:50
  • Which is the great flaw in this question, he should be using MemoryCache instead. – Hans Passant Sep 08 '13 at 16:42
  • @HansPassant have you removed your answer? – Mare Infinitus Sep 08 '13 at 16:57
  • Somewhat related: [Enforce an async method to be called lazily on demand, and called again when the previous result has expired](https://stackoverflow.com/questions/68467426/enforce-an-async-method-to-be-called-lazily-on-demand-and-called-again-when-the) – Theodor Zoulias Jul 22 '21 at 07:36

3 Answers3

8

2021 edit:

You probably shouldn't use this code. Maybe it's good enough for a very simple app, but it has some issues. It doesn't support cache invalidation, it may have issues related to DST or other time zone changes due to the way it uses DateTime, and it uses locks in a way that's safe but potentially quite slow. Consider using something like MemoryCache instead.

Original answer:

I agree with the other commenters that you probably shouldn't touch Lazy at all. Lazy isn't very complicated if you ignore the multiple thread-safety options, so just implement it from scratch.

I quite like the idea by the way, although I don't know if I'd be comfortable using it as a general purpose caching strategy. It might be sufficient for some of the simpler scenarios.

Here's my stab at it. If you don't need it to be thread-safe you can just remove the locking stuff. I don't think it's possible to use the double-checking lock pattern here because of the chance that the cached value may be be invalidated inside the lock.

public class Temporary<T>
{
    private readonly Func<T> factory;
    private readonly TimeSpan lifetime;
    private readonly object valueLock = new object();

    private T value;
    private bool hasValue;
    private DateTime creationTime;
    
    public Temporary(Func<T> factory, TimeSpan lifetime)
    {
        this.factory = factory;
        this.lifetime = lifetime;
    }
    
    public T Value
    {
        get
        {
            DateTime now = DateTime.Now;
            lock (this.valueLock)
            {
                if (this.hasValue)
                {
                    if (this.creationTime.Add(this.lifetime) < now)
                    {
                        this.hasValue = false;
                    }
                }
                
                if (!this.hasValue)
                {
                    this.value = this.factory();
                    this.hasValue = true;

                    // You can also use the existing "now" variable here.
                    // It depends on when you want the cache time to start
                    // counting from.
                    this.creationTime = Datetime.Now;
                }
                
                return this.value;
            }
        }
    }
}
Magnus Grindal Bakken
  • 2,083
  • 1
  • 16
  • 22
  • Is this still the best way of doing this in 2021? I find this class very useful but the answer is quite old so if this has been incorporated into .net or something it would be good to know – Craig May 10 '21 at 11:49
  • @Craig If your goal is to cache values to save computation time, you're probably better off using MemoryCache from System.Runtime.Caching, or some other caching library. The code in my answer has potential issues with time zones and clock synchronization, and there's no way to invalidate the cached value. – Magnus Grindal Bakken May 11 '21 at 05:38
  • `DateTime.Now` - don't use that. DST will cause you fun times. :) There are also some typos in your code. – mjwills May 11 '21 at 05:46
  • @MagnusGrindalBakken MemoryCache is what I have been looking for, thank you – Craig May 11 '21 at 12:40
3

I don't think Lazy<T> would have any influence here, it's more like a general approach, essentially being similar to the singleton pattern.

You'll need a simple wrapper class which will either return the real object or pass all calls to it.

I'd try something like this (out of memory, so might include bugs):

public class Timed<T> where T : new() {
    DateTime init;
    T obj;

    public Timed() {
        init = new DateTime(0);
    }

    public T get() {
        if (DateTime.Now - init > max_lifetime) {
            obj = new T();
            init = DateTime.Now;
        }
        return obj;
    }
}

To use, you'd then just use Timed<MyClass> obj = new Timed<MyClass>(); rather than MyClass obj = new MyClass();. And actual calls would be obj.get().doSomething() instead of obj.doSomething().

Edit:

Just to note, you won't have to combine an approach similar to mine above with Lazy<T> because you're essentially forcing a delayed initialization already. You could of course define the maximum lifetime in the constructor for example.

Mario
  • 35,726
  • 5
  • 62
  • 78
  • I believe you need to restrict T to `where T : new()` in this case. – Omada Sep 08 '13 at 15:15
  • Hm, not really sure. Don't think I've ever used generics in C#. Edit: Yes, seems like you're right. – Mario Sep 08 '13 at 15:15
  • Also, `return object` is a compile error. You meant `return init` right? And `get` would be better suited as a property. The effect is the same, but it's more idiomatic that way. – siride Sep 08 '13 at 15:29
  • For that matter, the `void` parameter list is not valid C# either. – Trillian Sep 08 '13 at 15:34
  • `return object` has been a leftover, forgot about the built in type at first and forgot to change that line. `void`, you're right. As for a getter... what to name it? But yes, lots of different approaches. You could as well skip the whole `get()` and just wrap all members using some predefined interface as well. – Mario Sep 08 '13 at 15:36
3

I needed the same thing. But I would prefer an implementation without locked reads when there is no write.

public class ExpiringLazy<T>
{
    private readonly Func<T> factory;
    private readonly TimeSpan lifetime;
    private readonly ReaderWriterLockSlim locking = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);

    private T value;
    private DateTime expiresOn = DateTime.MinValue;

    public ExpiringLazy(Func<T> factory, TimeSpan lifetime)
    {
        this.factory = factory;
        this.lifetime = lifetime;
    }

    public T Value
    {
        get
        {
            DateTime now = DateTime.UtcNow;
            locking.EnterUpgradeableReadLock();
            try
            {
                if (expiresOn < now)
                {
                    locking.EnterWriteLock();
                    try
                    {
                        if (expiresOn < now)
                        {
                            value = factory();
                            expiresOn = DateTime.UtcNow.Add(lifetime);
                        }
                    }
                    finally
                    {
                        locking.ExitWriteLock();
                    }
                }

                return value;
            }
            finally
            {
                locking.ExitUpgradeableReadLock();
            }
        }
    }
}
Pietro
  • 413
  • 2
  • 11