/// <summary>
        /// This method attempts to returned a cached value, and fetch one from
        /// the web. Optionally, it can continue to query to see if an update is required.
        ///
        /// If the cached value exists, it is returned. Then the predicate is queried to see
        /// if the remote value should be refreshed.
        ///
        /// If there is no cached value, then the value is fetched.
        ///
        /// Once the above is done, as the retrySequence comes in, the predicate will
        /// be called to see if a refresh is needed. If so, the data will be re-fetched.
        ///
        /// In all cases any remotely fetched data is cached.
        ///
        /// This also means that await'ing this method is a Bad Idea(tm), always
        /// use Subscribe. 1-infinity values can be returned depending on the arguments.
        /// </summary>
        /// <param name="key">The key to store the returned result under.</param>
        /// <param name="fetchFunc">A sequence that will return the new values of the data</param>
        /// <param name="fetchPredicate">A Func to determine whether
        /// the updated item should be fetched. Only called once a cached version exists.</param>
        /// <param name="retrySequence">Sequence that will trigger a predicate call followed by
        /// a fetch call if the predicate indicates so.</param>
        /// <param name="This">The blob cache against which we operate</param>
        /// <returns>An Observable stream containing one or more
        /// results (possibly a cached version, then the latest version(s))</returns>
        public static IObservable <T> GetAndFetchLatest <T>(this IBlobCache This,
                                                            string key,
                                                            Func <IObservable <T> > fetchFunc,
                                                            Func <DateTimeOffset, IObservable <bool> > fetchPredicate,
                                                            IObservable <Unit> retrySequence  = null,
                                                            DateTimeOffset?absoluteExpiration = null
                                                            )
        {
            if (fetchPredicate == null)
            {
                throw new ArgumentException("fetchPredicate");
            }
            if (fetchFunc == null)
            {
                throw new ArgumentException("fetchFunc");
            }

            // We are going to get the cache value if we can. And then we will run updates after that.
            // If we have nothing cached, then we will run the fetch directly. Otherwise we will run the
            // fetch sequence.

            var getOldKey = This.GetObjectCreatedAt <T>(key);

            var refetchIfNeeded = getOldKey
                                  .Where(dt => dt != null && dt.HasValue && dt.Value != null)
                                  .SelectMany(dt => fetchPredicate(dt.Value))
                                  .Where(doit => doit == true)
                                  .Select(_ => default(Unit));

            var fetchRequired = getOldKey
                                .Where(dt => dt == null || !dt.HasValue || dt.Value == null)
                                .Select(_ => default(Unit));

            // Next, get the item...

            var fetchFromCache = Observable.Defer(() => This.GetObject <T>(key))
                                 .Catch <T, KeyNotFoundException>(_ => Observable.Empty <T>());

            var fetchFromRemote = fetchRequired.Concat(refetchIfNeeded)
                                  .SelectMany(_ => fetchFunc())
                                  .SelectMany(x => This.InsertObject <T>(key, x, absoluteExpiration).Select(_ => x));

            var items = fetchFromCache.Concat(fetchFromRemote);

            // Once we have these, we also have to kick off a second set of fetches for our retry sequence.
            if (retrySequence == null)
            {
                return(items);
            }

            var getAfter = retrySequence
                           .SelectMany(_ => This.GetObjectCreatedAt <T>(key))
                           .SelectMany(dt => fetchPredicate(dt.Value))
                           .Where(doit => doit == true)
                           .SelectMany(_ => fetchFunc())
                           .SelectMany(x => This.InsertObject <T>(key, x, absoluteExpiration).Select(_ => x));

            return(items.Concat(getAfter));
        }
        /// <summary>
        /// This method attempts to returned a cached value, while
        /// simultaneously calling a Func to return the latest value. When the
        /// latest data comes back, it replaces what was previously in the
        /// cache.
        ///
        /// This method is best suited for loading dynamic data from the
        /// Internet, while still showing the user earlier data.
        ///
        /// This method returns an IObservable that may return *two* results
        /// (first the cached data, then the latest data). Therefore, it's
        /// important for UI applications that in your Subscribe method, you
        /// write the code to merge the second result when it comes in.
        ///
        /// This also means that await'ing this method is a Bad Idea(tm), always
        /// use Subscribe.
        /// </summary>
        /// <param name="key">The key to store the returned result under.</param>
        /// <param name="fetchFunc"></param>
        /// <param name="fetchPredicate">An optional Func to determine whether
        /// the updated item should be fetched. If the cached version isn't found,
        /// this parameter is ignored and the item is always fetched.</param>
        /// <param name="absoluteExpiration">An optional expiration date.</param>
        /// <param name="shouldInvalidateOnError">If this is true, the cache will
        /// be cleared when an exception occurs in fetchFunc</param>
        /// <returns>An Observable stream containing either one or two
        /// results (possibly a cached version, then the latest version)</returns>
        public static IObservable <T> GetAndFetchLatest <T>(this IBlobCache This,
                                                            string key,
                                                            Func <IObservable <T> > fetchFunc,
                                                            Func <DateTimeOffset, bool> fetchPredicate = null,
                                                            DateTimeOffset?absoluteExpiration          = null,
                                                            bool shouldInvalidateOnError = false)
        {
            var fetch = Observable.Defer(() => This.GetObjectCreatedAt <T>(key))
                        .Select(x => fetchPredicate == null || x == null || fetchPredicate(x.Value))
                        .Where(x => x != false)
                        .SelectMany(_ =>
            {
                var fetchObs = fetchFunc().Catch <T, Exception>(ex =>
                {
                    var shouldInvalidate = shouldInvalidateOnError ?
                                           This.InvalidateObject <T>(key) :
                                           Observable.Return(Unit.Default);
                    return(shouldInvalidate.SelectMany(__ => Observable.Throw <T>(ex)));
                });

                return(fetchObs
                       .SelectMany(x => This.InvalidateObject <T>(key).Select(__ => x))
                       .SelectMany(x => This.InsertObject <T>(key, x, absoluteExpiration).Select(__ => x)));
            });

            var result = This.GetObject <T>(key).Select(x => new Tuple <T, bool>(x, true))
                         .Catch(Observable.Return(new Tuple <T, bool>(default(T), false)));

            return(result.SelectMany(x =>
            {
                return x.Item2 ?
                Observable.Return(x.Item1) :
                Observable.Empty <T>();
            }).Concat(fetch).Multicast(new ReplaySubject <T>()).RefCount());
        }
        public static IObservable <T> GetAndRequestFetch <T>(this IBlobCache This,
                                                             string key,
                                                             Func <IObservable <T> > fetchFunc,
                                                             Func <DateTimeOffset?, IObservable <bool> > fetchPredicate,
                                                             IObservable <Unit> requestSequence = null,
                                                             DateTimeOffset?absoluteExpiration  = null
                                                             )
        {
            if (fetchPredicate == null)
            {
                throw new ArgumentException("fetchPredicate");
            }
            if (fetchFunc == null)
            {
                throw new ArgumentException("fetchFunc");
            }

            // We are going to get the cache value if we can. And then we will run updates after that.
            // If we have nothing cached, then we will run the fetch directly. Otherwise we will run the
            // fetch sequence.

            var getOldKey = This.GetObjectCreatedAt <T>(key);

            // Next, get the item...

            var fetchFromCache = Observable.Defer(() => This.GetObject <T>(key))
                                 .Catch <T, KeyNotFoundException>(_ => Observable.Empty <T>());

            var items = fetchFromCache;

            // Once we have these, we also have to kick off a second set of fetches for our retry sequence.
            if (requestSequence == null)
            {
                return(items);
            }

            // TODO: How can we make this atomic. THe problem is that fetchPredicate may depend on the object having been
            // inserted, but because they are on different threads we may get race conditions. So there must be a way...
            var getAfter = requestSequence
                           .SelectMany(_ => This.GetObjectCreatedAt <T>(key))
                           .SelectMany(dt => fetchPredicate(dt))
                           .Where(doit => doit == true)
                           .SelectMany(_ => fetchFunc())
                           .SelectMany(x => This.InsertObject <T>(key, x, absoluteExpiration).Select(_ => x));

            return(items.Concat(getAfter));
        }
Example #4
0
        public static IObservable <T?> GetAndFetchLatest <T>(
            this IBlobCache blobCache,
            string key,
            Func <IObservable <T> > fetchFunc,
            Func <DateTimeOffset, bool>?fetchPredicate = null,
            DateTimeOffset?absoluteExpiration          = null,
            bool shouldInvalidateOnError            = false,
            Func <T, bool>?cacheValidationPredicate = null)
        {
            if (blobCache is null)
            {
                throw new ArgumentNullException(nameof(blobCache));
            }

#pragma warning disable CS8604 // Possible null reference argument.
            var fetch = Observable.Defer(() => blobCache.GetObjectCreatedAt <T>(key))
                        .Select(x => fetchPredicate is null || x is null || fetchPredicate(x.Value))
                        .Where(x => x)
                        .SelectMany(_ =>
            {
                var fetchObs = fetchFunc().Catch <T, Exception>(ex =>
                {
                    var shouldInvalidate = shouldInvalidateOnError ?
                                           blobCache.InvalidateObject <T>(key) :
                                           Observable.Return(Unit.Default);
                    return(shouldInvalidate.SelectMany(__ => Observable.Throw <T>(ex)));
                });

                return(fetchObs
                       .SelectMany(x =>
                                   cacheValidationPredicate is not null && !cacheValidationPredicate(x)
                                ? Observable.Return(default(T))
                                : blobCache.InvalidateObject <T>(key).Select(__ => x))
                       .SelectMany(x =>
                                   cacheValidationPredicate is not null && !cacheValidationPredicate(x)
                                ? Observable.Return(default(T))
                                : blobCache.InsertObject(key, x, absoluteExpiration).Select(__ => x)));
            });

            if (fetch is null)
            {
                return(Observable.Throw <T>(new Exception("Could not find a valid way to fetch the value")));
            }

            var result = blobCache.GetObject <T>(key).Select(x => (x, true))
                         .Catch(Observable.Return((default(T), false)));

#pragma warning restore CS8604 // Possible null reference argument.

            return(result.SelectMany(x => x.Item2 ? Observable.Return(x.Item1) : Observable.Empty <T>())
                   .Concat(fetch)
                   .Multicast(new ReplaySubject <T?>())
                   .RefCount());
        }
Example #5
0
        /// <summary>
        /// This method attempts to returned a cached value, while
        /// simultaneously calling a Func to return the latest value. When the
        /// latest data comes back, it replaces what was previously in the
        /// cache.
        ///
        /// This method is best suited for loading dynamic data from the
        /// Internet, while still showing the user earlier data.
        ///
        /// This method returns an IObservable that may return *two* results
        /// (first the cached data, then the latest data). Therefore, it's
        /// important for UI applications that in your Subscribe method, you
        /// write the code to merge the second result when it comes in.
        ///
        /// This also means that await'ing this method is a Bad Idea(tm), always
        /// use Subscribe.
        /// </summary>
        /// <param name="key">The key to store the returned result under.</param>
        /// <param name="fetchFunc"></param>
        /// <param name="fetchPredicate">An optional Func to determine whether
        /// the updated item should be fetched. If the cached version isn't found,
        /// this parameter is ignored and the item is always fetched.</param>
        /// <param name="absoluteExpiration">An optional expiration date.</param>
        /// <param name="shouldInvalidateOnError">If this is true, the cache will
        /// be cleared when an exception occurs in fetchFunc</param>
        /// <returns>An Observable stream containing either one or two
        /// results (possibly a cached version, then the latest version)</returns>
        public static IObservable <T> GetAndFetchLatest <T>(this IBlobCache This,
                                                            string key,
                                                            Func <IObservable <T> > fetchFunc,
                                                            Func <DateTimeOffset, bool> fetchPredicate = null,
                                                            DateTimeOffset?absoluteExpiration          = null,
                                                            bool shouldInvalidateOnError = false)
        {
            var fetch = Observable.Defer(() => This.GetObjectCreatedAt <T>(key))
                        .Select(x => fetchPredicate == null || x == null || fetchPredicate(x.Value))
                        .Where(x => x != false)
                        .SelectMany(async _ => {
                var ret = default(T);
                try {
                    ret = await fetchFunc();
                } catch (Exception) {
                    if (shouldInvalidateOnError)
                    {
                        This.InvalidateObject <T>(key);
                    }
                    throw;
                }

                await This.InvalidateObject <T>(key);
                await This.InsertObject(key, ret, absoluteExpiration);
                return(ret);
            });

            var result = This.GetObjectAsync <T>(key).Select(x => new Tuple <T, bool>(x, true))
                         .Catch(Observable.Return(new Tuple <T, bool>(default(T), false)));

            return(result.SelectMany(x =>
            {
                return x.Item2 ?
                Observable.Return(x.Item1) :
                Observable.Empty <T>();
            }).Concat(fetch).Multicast(new ReplaySubject <T>()).RefCount());
        }
 /// <summary>
 /// Returns the time that this particular object was put into the cache.
 /// </summary>
 /// <param name="file"></param>
 /// <returns></returns>
 public static IObservable <DateTimeOffset?> GetCacheCreateTime(this IFile file, IBlobCache cache = null)
 {
     cache = cache ?? Blobs.LocalStorage;
     return(cache.GetObjectCreatedAt <string>(file.FileDateKey()));
 }