Skip to content

Distributed cache #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -190,3 +190,4 @@ $RECYCLE.BIN/

packages/
.vs/
/.vscode
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using LazyCache;
using Microsoft.AspNetCore.Mvc;

namespace CacheDatabaseQueriesApiSample.Controllers
{
public class DbTimeDistributedController : Controller
{
private readonly IDistributedAppCache _distributedCache;
private readonly string cacheKey = "DbTimeController.Get";
private readonly DbTimeContext dbContext;


public DbTimeDistributedController(DbTimeContext context, IDistributedAppCache distributedCache)
{
dbContext = context;
_distributedCache = distributedCache;
}

[HttpGet]
[Route("api/ddbtime")]
public DbTimeEntity Get()
{
Func<DbTimeEntity> actionThatWeWantToCache = () => dbContext.GeDbTime();

var cachedDatabaseTime = _distributedCache.GetOrAdd(cacheKey, actionThatWeWantToCache);

return cachedDatabaseTime;
}

[HttpDelete]
[Route("api/ddbtime")]
public IActionResult DeleteFromCache()
{
_distributedCache.Remove(cacheKey);
var friendlyMessage = new { Message = $"Item with key '{cacheKey}' removed from server in-memory cache" };
return Ok(friendlyMessage);
}
}
}
2 changes: 1 addition & 1 deletion CacheDatabaseQueriesApiSample/DbTimeContext.cs
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ public DbTimeEntity GeDbTime()
// get the current time from SQL server right now asynchronously (simulating a slow query)
var result = Times
.FromSql("WAITFOR DELAY '00:00:00:500'; SELECT 1 as [ID], GETDATE() as [TimeNowInTheDatabase]")
.Single();
.SingleOrDefault();

databaseRequestCounter++;

11 changes: 10 additions & 1 deletion CacheDatabaseQueriesApiSample/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using Microsoft.AspNetCore.Builder;
using LazyCache;
using LazyCache.Providers;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

@@ -29,6 +32,12 @@ public void ConfigureServices(IServiceCollection services)

// Register IAppCache as a singleton CachingService
services.AddLazyCache();

services.AddMemoryCache();
services.AddDistributedMemoryCache();
services.AddSingleton<DistributedCacheProvider>();
services.AddTransient<IDistributedCacheProvider>(provider => new HybridCacheProvider(provider.GetRequiredService<DistributedCacheProvider>(), provider.GetRequiredService<IMemoryCache>()));
services.AddDistributedHybridLazyCache(provider => new HybridCachingService(provider.GetRequiredService<IDistributedCacheProvider>()));
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
159 changes: 159 additions & 0 deletions CacheDatabaseQueriesApiSample/wwwroot/index-d.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="IE=edge" http-equiv="X-UA-Compatible">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Lazy cache sample app</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"> </script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"> </script>
<![endif]-->
</head>
<body>
<div class="container">
<div class="centre-box">
<div class="counter pull-right"><span class="counter-val">0</span> Database query(s)</div>

<h1>Sample app to demonstrate using a distributed cache in your API to save database SQL queries and speed up API calls</h1>

<p>
Every 3 seconds we fetch the current time from the database, however because the sql query
result is cached on the server on the first call, the time stays the same untill you clear
the cache and no more SQL queries are made. <br />
<br />

After you clear the cache you should see the time change because a real SQL query is made.<br />
<br />

Also note how real SQL queryies are slower than cache hits.
</p>

<div class="button-bar">
<button class="btn btn-default btn-primary btn-clear-cache">Clear Cache</button>
</div>

<label>Log</label>
<p class="log"></p>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
<script>
// util for n digit numbers with leading zeros
Number.prototype.pad = function (len) {
return (new Array(len + 1).join("0") + this).slice(-len);
}
</script>
<script>
"use strict";

(function () {
var app = {
log: function (message) {
var now = new Date();
var prefix = "[" + now.getHours().pad(2) + ":" + now.getMinutes().pad(2) + ":" + now.getSeconds().pad(2) + "] ";
var currentLog = $(".log").text();
$(".log").text(prefix + message + "\n" + currentLog);
},

getDbTime: function () {
return $.get("/api/ddbtime");
},

updateDbQueryCount: function () {
return $.get("/api/dbQueries")
.done(function (data) {
$(".counter-val").text(data);
});
},

updateLogWithTime: function (dbTime, duration) {
var message = "API reports that the time now in the database is " + dbTime.timeNowInTheDatabase + " (" + duration + "ms)";
app.log(message);
},

clearCache: function () {
$.ajax({
method: "DELETE",
url: "/api/ddbtime"
})
.done(function (data) {
app.log(data.message);
});
},

updateTimeEvent: function () {

var start_time = new Date().getTime();

return app.getDbTime()
.done(function(data) {
var duration = new Date().getTime() - start_time;
app.updateLogWithTime(data, duration);
})
.then(app.updateDbQueryCount);
},

startPollingForTime: function () {
var threeSecs = 3000;
setInterval(app.updateTimeEvent, threeSecs);
},

bindEvents: function () {
$(".btn-clear-cache").click(app.clearCache);

$(document).ajaxError(function (event, request, settings) {
app.log("Error requesting page " + settings.url);
});
}

};

// setup the application
app.bindEvents();

// fetch the db time now
app.log("Starting app... Fetching the current time from SQL server every 3 secs");
app.updateTimeEvent()
.then(app.startPollingForTime);
})();

</script>

<style>
body {
background-color: #EEEEEE;
padding-bottom: 40px;
padding-top: 40px;
}

.centre-box {
margin: 0 auto;
max-width: 850px;
padding: 15px;
}

.log {
font-family: consolas;
white-space: pre-line;
}

.button-bar {
min-height: 3em;
}

.counter {
margin: -3em 0 0 0;
padding: 0.5em;
max-width: 6em;
background-color: darkslategrey;
color: lightskyblue;
text-align: center;
font-size: 1.2em;
font-weight: bold;
}
</style>
</body>
</html>
9 changes: 9 additions & 0 deletions Console.Net461/Program.cs
Original file line number Diff line number Diff line change
@@ -4,6 +4,10 @@
using System.Text;
using System.Threading.Tasks;
using LazyCache;
using LazyCache.Providers;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Ninject;

namespace Console.Net461
@@ -26,6 +30,11 @@ static void Main(string[] args)
item = cache.GetOrAdd("Program.Main.Person", () => Tuple.Create("Joe Blogs", DateTime.UtcNow));

System.Console.WriteLine(item.Item1);

IDistributedAppCache distributedCache = new HybridCachingService(new HybridCacheProvider(new DistributedCacheProvider(new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions()))), new MemoryCache(Options.Create(new MemoryCacheOptions()))));
item = distributedCache.GetOrAdd("Program.Main.Person", () => Tuple.Create("Joe Blogs", DateTime.UtcNow));

System.Console.WriteLine(item.Item1);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using LazyCache;
using LazyCache.Providers;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection.Extensions;

// ReSharper disable once CheckNamespace - MS guidelines say put DI registration in this NS
namespace Microsoft.Extensions.DependencyInjection
{
// See https://github.com/aspnet/Caching/blob/dev/src/Microsoft.Extensions.Caching.Memory/MemoryCacheServiceCollectionExtensions.cs
public static class DistributedLazyCacheServiceCollectionExtensions
{
public static IServiceCollection AddDistributedLazyCache(this IServiceCollection services)
{
if (services == null) throw new ArgumentNullException(nameof(services));

services.AddOptions();
services.TryAddSingleton<IDistributedCache, MemoryDistributedCache>();
services.TryAddSingleton<IDistributedCacheProvider, DistributedCacheProvider>();
services.TryAddSingleton<IDistributedAppCache, DistributedCachingService>();

return services;
}

public static IServiceCollection AddDistributedLazyCache(this IServiceCollection services,
Func<IServiceProvider, DistributedCachingService> implementationFactory)
{
if (services == null) throw new ArgumentNullException(nameof(services));
if (implementationFactory == null) throw new ArgumentNullException(nameof(implementationFactory));

services.AddOptions();
services.TryAddSingleton<IDistributedCacheProvider, DistributedCacheProvider>();
services.TryAddSingleton<IDistributedAppCache>(implementationFactory);

return services;
}

public static IServiceCollection AddDistributedHybridLazyCache(this IServiceCollection services)
{
if (services == null) throw new ArgumentNullException(nameof(services));

services.AddOptions();
services.TryAddSingleton<IMemoryCache, MemoryCache>();
services.TryAddSingleton<IDistributedCache, MemoryDistributedCache>();
services.TryAddSingleton<DistributedCacheProvider>();
services.TryAddSingleton<IDistributedCacheProvider>(provider => new HybridCacheProvider(provider.GetRequiredService<DistributedCacheProvider>(), provider.GetRequiredService<IMemoryCache>()));
services.TryAddSingleton<IDistributedAppCache, HybridCachingService>();

return services;
}

public static IServiceCollection AddDistributedHybridLazyCache(this IServiceCollection services,
Func<IServiceProvider, HybridCachingService> implementationFactory)
{
if (services == null) throw new ArgumentNullException(nameof(services));
if (implementationFactory == null) throw new ArgumentNullException(nameof(implementationFactory));

services.AddOptions();
services.TryAddSingleton<IMemoryCache, MemoryCache>();
services.TryAddSingleton<DistributedCacheProvider>();
services.TryAddSingleton<IDistributedCacheProvider>(provider => new HybridCacheProvider(provider.GetRequiredService<DistributedCacheProvider>(), provider.GetRequiredService<IMemoryCache>()));
services.TryAddSingleton<IDistributedAppCache>(implementationFactory);

return services;
}
}
}
706 changes: 706 additions & 0 deletions LazyCache.UnitTests/DistributedCachingServiceProviderTests.cs

Large diffs are not rendered by default.

94 changes: 94 additions & 0 deletions LazyCache/DistributedAppCacheExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;

namespace LazyCache
{
public static class DistributedAppCacheExtenions

{
public static void Add<T>(this IDistributedAppCache cache, string key, T item)
{
if (cache == null) throw new ArgumentNullException(nameof(cache));

cache.Add(key, item, cache.DefaultCachePolicy.BuildOptions());
}

public static void Add<T>(this IDistributedAppCache cache, string key, T item, DateTimeOffset expires)
{
if (cache == null) throw new ArgumentNullException(nameof(cache));

cache.Add(key, item, new DistributedCacheEntryOptions { AbsoluteExpiration = expires });
}

public static void Add<T>(this IDistributedAppCache cache, string key, T item, TimeSpan slidingExpiration)
{
if (cache == null) throw new ArgumentNullException(nameof(cache));

cache.Add(key, item, new DistributedCacheEntryOptions { SlidingExpiration = slidingExpiration });
}

public static T GetOrAdd<T>(this IDistributedAppCache cache, string key, Func<T> addItemFactory)
{
if (cache == null) throw new ArgumentNullException(nameof(cache));

return cache.GetOrAdd(key, addItemFactory, cache.DefaultCachePolicy.BuildOptions());
}

public static T GetOrAdd<T>(this IDistributedAppCache cache, string key, Func<T> addItemFactory, DateTimeOffset expires)
{
if (cache == null) throw new ArgumentNullException(nameof(cache));

return cache.GetOrAdd(key, addItemFactory, new DistributedCacheEntryOptions { AbsoluteExpiration = expires });
}

public static T GetOrAdd<T>(this IDistributedAppCache cache, string key, Func<T> addItemFactory, TimeSpan slidingExpiration)
{
return cache.GetOrAdd(key, addItemFactory,
new DistributedCacheEntryOptions { SlidingExpiration = slidingExpiration });
}

public static T GetOrAdd<T>(this IDistributedAppCache cache, string key, Func<T> addItemFactory, DistributedCacheEntryOptions policy)
{
if (cache == null) throw new ArgumentNullException(nameof(cache));

return cache.GetOrAdd(key, entry =>
{
entry.SetOptions(policy);
return addItemFactory();
});
}

public static Task<T> GetOrAddAsync<T>(this IDistributedAppCache cache, string key, Func<Task<T>> addItemFactory)
{
if (cache == null) throw new ArgumentNullException(nameof(cache));

return cache.GetOrAddAsync(key, addItemFactory, cache.DefaultCachePolicy.BuildOptions());
}

public static Task<T> GetOrAddAsync<T>(this IDistributedAppCache cache, string key, Func<Task<T>> addItemFactory, DateTimeOffset expires)
{
if (cache == null) throw new ArgumentNullException(nameof(cache));

return cache.GetOrAddAsync(key, addItemFactory, new DistributedCacheEntryOptions { AbsoluteExpiration = expires });
}

public static Task<T> GetOrAddAsync<T>(this IDistributedAppCache cache, string key, Func<Task<T>> addItemFactory, TimeSpan slidingExpiration)
{
if (cache == null) throw new ArgumentNullException(nameof(cache));

return cache.GetOrAddAsync(key, addItemFactory, new DistributedCacheEntryOptions { SlidingExpiration = slidingExpiration });
}

public static Task<T> GetOrAddAsync<T>(this IDistributedAppCache cache, string key, Func<Task<T>> addItemFactory, DistributedCacheEntryOptions policy)
{
if (cache == null) throw new ArgumentNullException(nameof(cache));

return cache.GetOrAddAsync(key, entry =>
{
entry.SetOptions(policy);
return addItemFactory();
});
}
}
}
19 changes: 19 additions & 0 deletions LazyCache/DistributedCacheDefaults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using Microsoft.Extensions.Caching.Distributed;

namespace LazyCache
{
public class DistributedCacheDefaults
{
public virtual int DefaultCacheDurationSeconds { get; set; } = 60 * 20;

internal DistributedCacheEntryOptions BuildOptions()
{
return new DistributedCacheEntryOptions
{
AbsoluteExpiration = DateTimeOffset.UtcNow.AddSeconds(DefaultCacheDurationSeconds),
SlidingExpiration = TimeSpan.FromSeconds(200)
};
}
}
}
38 changes: 38 additions & 0 deletions LazyCache/DistributedCacheEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using LazyCache;
using Microsoft.Extensions.Caching.Distributed;

public sealed class DistributedCacheEntry
{
public string Key { get; internal set; }

public object Value { get; internal set; }

public DistributedCacheEntryOptions DistributedCacheEntryOptions { get; private set; }


public void SetOptions(DistributedCacheEntryOptions options)
{
DistributedCacheEntryOptions = options;
}

public DistributedCacheEntry(string key, object value, DistributedCacheEntryOptions distributedCacheEntryOptions) : this(key, distributedCacheEntryOptions)
{
Value = value;
}

public DistributedCacheEntry(string key, DistributedCacheEntryOptions distributedCacheEntryOptions) : this(key)
{
DistributedCacheEntryOptions = distributedCacheEntryOptions;
}

public DistributedCacheEntry(string key)
{
Key = key;
DistributedCacheEntryOptions = new DistributedCacheDefaults().BuildOptions();
}

public void SetValue(object value)
{
Value = value;
}
}
196 changes: 196 additions & 0 deletions LazyCache/DistributedCachingService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
using Microsoft.Extensions.Caching.Distributed;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace LazyCache
{
public class DistributedCachingService : IDistributedAppCache
{
private readonly Lazy<IDistributedCacheProvider> cacheProvider;

private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1);

public DistributedCachingService(Lazy<IDistributedCacheProvider> cacheProvider)
{
this.cacheProvider = cacheProvider ?? throw new ArgumentNullException(nameof(cacheProvider));
}

public DistributedCachingService(Func<IDistributedCacheProvider> cacheProviderFactory)
{
if (cacheProviderFactory == null) throw new ArgumentNullException(nameof(cacheProviderFactory));
cacheProvider = new Lazy<IDistributedCacheProvider>(cacheProviderFactory);
}

public DistributedCachingService(IDistributedCacheProvider cache) : this(() => cache)
{
if (cache == null) throw new ArgumentNullException(nameof(cache));
}

/// <summary>
/// Seconds to cache objects for by default
/// </summary>
[Obsolete("DefaultCacheDuration has been replaced with DefaultCacheDurationSeconds")]
public virtual int DefaultCacheDuration
{
get => DefaultCachePolicy.DefaultCacheDurationSeconds;
set => DefaultCachePolicy.DefaultCacheDurationSeconds = value;
}

public virtual IDistributedCacheProvider DistributedCacheProvider => cacheProvider.Value;

/// <summary>
/// Policy defining how long items should be cached for unless specified
/// </summary>
public virtual DistributedCacheDefaults DefaultCachePolicy { get; set; } = new DistributedCacheDefaults();

public virtual void Add<T>(string key, T item, DistributedCacheEntryOptions policy)
{
if (item == null)
throw new ArgumentNullException(nameof(item));
ValidateKey(key);

DistributedCacheProvider.Set(key, item, policy);
}

public virtual T Get<T>(string key)
{
ValidateKey(key);

var item = DistributedCacheProvider.Get<T>(key);

return GetValueFromLazy<T>(item);
}

public virtual Task<T> GetAsync<T>(string key)
{
ValidateKey(key);

var item = DistributedCacheProvider.Get(key);

return GetValueFromAsyncLazy<T>(item);
}

public virtual T GetOrAdd<T>(string key, Func<DistributedCacheEntry, T> addItemFactory)
{
ValidateKey(key);

object cacheItem;
locker.Wait(); //TODO: do we really need this? Could we just lock on the key?

try
{
var value = (T)DistributedCacheProvider.GetOrCreate(key, addItemFactory);
cacheItem = new Lazy<T>(() => value);
}
finally
{
locker.Release();
}

try
{
return GetValueFromLazy<T>(cacheItem);
}
catch //addItemFactory errored so do not cache the exception
{
DistributedCacheProvider.Remove(key);
throw;
}
}

public virtual void Remove(string key)
{
ValidateKey(key);
DistributedCacheProvider.Remove(key);
}


public virtual async Task<T> GetOrAddAsync<T>(string key, Func<DistributedCacheEntry, Task<T>> addItemFactory)
{
ValidateKey(key);

object cacheItem;

// Ensure only one thread can place an item into the cache provider at a time.
// We are not evaluating the addItemFactory inside here - that happens outside the lock,
// below, and guarded using the async lazy. Here we just ensure only one thread can place
// the AsyncLazy into the cache at one time

await locker.WaitAsync()
.ConfigureAwait(
false); //TODO: do we really need to lock everything here - faster if we could lock on just the key?
try
{
var value = await DistributedCacheProvider.GetOrCreateAsync(key, addItemFactory);
cacheItem = new Lazy<T>(() => (T) value);
}
finally
{
locker.Release();
}

try
{
var result = GetValueFromAsyncLazy<T>(cacheItem);

if (result.IsCanceled || result.IsFaulted)
DistributedCacheProvider.Remove(key);

return await result.ConfigureAwait(false);
}
catch //addItemFactory errored so do not cache the exception
{
DistributedCacheProvider.Remove(key);
throw;
}
}

protected virtual T GetValueFromLazy<T>(object item)
{
switch (item)
{
case Lazy<T> lazy:
return lazy.Value;
case T variable:
return variable;
case AsyncLazy<T> asyncLazy:
// this is async to sync - and should not really happen as long as GetOrAddAsync is used for an async
// value. Only happens when you cache something async and then try and grab it again later using
// the non async methods.
return asyncLazy.Value.ConfigureAwait(false).GetAwaiter().GetResult();
case Task<T> task:
return task.Result;
}

return default(T);
}

protected virtual Task<T> GetValueFromAsyncLazy<T>(object item)
{
switch (item)
{
case AsyncLazy<T> asyncLazy:
return asyncLazy.Value;
case Task<T> task:
return task;
// this is sync to async and only happens if you cache something sync and then get it later async
case Lazy<T> lazy:
return Task.FromResult(lazy.Value);
case T variable:
return Task.FromResult(variable);
}

return Task.FromResult(default(T));
}

protected virtual void ValidateKey(string key)
{
if (key == null)
throw new ArgumentNullException(nameof(key));

if (string.IsNullOrWhiteSpace(key))
throw new ArgumentOutOfRangeException(nameof(key), "Cache keys cannot be empty or whitespace");
}
}
}
215 changes: 215 additions & 0 deletions LazyCache/HybridCachingService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
using Microsoft.Extensions.Caching.Distributed;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace LazyCache
{
public class HybridCachingService : IDistributedAppCache
{
private readonly Lazy<IDistributedCacheProvider> cacheProvider;

private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1);

public HybridCachingService(Lazy<IDistributedCacheProvider> cacheProvider)
{
this.cacheProvider = cacheProvider ?? throw new ArgumentNullException(nameof(cacheProvider));
}

public HybridCachingService(Func<IDistributedCacheProvider> cacheProviderFactory)
{
if (cacheProviderFactory == null) throw new ArgumentNullException(nameof(cacheProviderFactory));
cacheProvider = new Lazy<IDistributedCacheProvider>(cacheProviderFactory);
}

public HybridCachingService(IDistributedCacheProvider cache) : this(() => cache)
{
if (cache == null) throw new ArgumentNullException(nameof(cache));
}

/// <summary>
/// Seconds to cache objects for by default
/// </summary>
[Obsolete("DefaultCacheDuration has been replaced with DefaultCacheDurationSeconds")]
public virtual int DefaultCacheDuration
{
get => DefaultCachePolicy.DefaultCacheDurationSeconds;
set => DefaultCachePolicy.DefaultCacheDurationSeconds = value;
}

public virtual IDistributedCacheProvider DistributedCacheProvider => cacheProvider.Value;

/// <summary>
/// Policy defining how long items should be cached for unless specified
/// </summary>
public virtual DistributedCacheDefaults DefaultCachePolicy { get; set; } = new DistributedCacheDefaults();

public virtual void Add<T>(string key, T item, DistributedCacheEntryOptions policy)
{
if (item == null)
throw new ArgumentNullException(nameof(item));
ValidateKey(key);

DistributedCacheProvider.Set(key, item, policy);
}

public virtual T Get<T>(string key)
{
ValidateKey(key);

var item = DistributedCacheProvider.Get<T>(key);

return GetValueFromLazy<T>(item);
}

public virtual Task<T> GetAsync<T>(string key)
{
ValidateKey(key);

var item = DistributedCacheProvider.Get(key);

return GetValueFromAsyncLazy<T>(item);
}

public virtual T GetOrAdd<T>(string key, Func<DistributedCacheEntry, T> addItemFactory)
{
ValidateKey(key);
DistributedCacheEntry temporaryCacheEntry = null;
object cacheItem;
locker.Wait(); //TODO: do we really need this? Could we just lock on the key?

try
{
cacheItem = DistributedCacheProvider.GetOrCreate<object>(key, entry =>
new Lazy<T>(() =>
{
temporaryCacheEntry = entry;
var result = addItemFactory(entry);
return result;
})
);
}
finally
{
locker.Release();
}

try
{
var toBeCached = GetValueFromLazy<T>(cacheItem);
DistributedCacheProvider.Set(key, toBeCached, temporaryCacheEntry != null ? temporaryCacheEntry.DistributedCacheEntryOptions : DefaultCachePolicy.BuildOptions());
return toBeCached;
}
catch //addItemFactory errored so do not cache the exception
{
DistributedCacheProvider.Remove(key);
throw;
}
}

public virtual void Remove(string key)
{
ValidateKey(key);
DistributedCacheProvider.Remove(key);
}


public virtual async Task<T> GetOrAddAsync<T>(string key, Func<DistributedCacheEntry, Task<T>> addItemFactory)
{
ValidateKey(key);

object cacheItem;
DistributedCacheEntry temporaryCacheEntry = null;
// Ensure only one thread can place an item into the cache provider at a time.
// We are not evaluating the addItemFactory inside here - that happens outside the lock,
// below, and guarded using the async lazy. Here we just ensure only one thread can place
// the AsyncLazy into the cache at one time

await locker.WaitAsync()
.ConfigureAwait(
false); //TODO: do we really need to lock everything here - faster if we could lock on just the key?
try
{
// var value = await DistributedCacheProvider.GetOrCreateAsync(key, addItemFactory);
// cacheItem = new Lazy<T>(() => (T) value);

cacheItem = DistributedCacheProvider.GetOrCreate<object>(key, entry =>
new AsyncLazy<T>(() =>
{
temporaryCacheEntry = entry;
var result = addItemFactory(entry);
return result;
})
);
}
finally
{
locker.Release();
}

try
{
var result = GetValueFromAsyncLazy<T>(cacheItem);

if (result.IsCanceled || result.IsFaulted)
DistributedCacheProvider.Remove(key);

var toBeCached = await result.ConfigureAwait(false);
DistributedCacheProvider.Set(key, toBeCached, temporaryCacheEntry != null ? temporaryCacheEntry.DistributedCacheEntryOptions : DefaultCachePolicy.BuildOptions());
return toBeCached;
}
catch //addItemFactory errored so do not cache the exception
{
DistributedCacheProvider.Remove(key);
throw;
}
}

protected virtual T GetValueFromLazy<T>(object item)
{
switch (item)
{
case Lazy<T> lazy:
return lazy.Value;
case T variable:
return variable;
case AsyncLazy<T> asyncLazy:
// this is async to sync - and should not really happen as long as GetOrAddAsync is used for an async
// value. Only happens when you cache something async and then try and grab it again later using
// the non async methods.
return asyncLazy.Value.ConfigureAwait(false).GetAwaiter().GetResult();
case Task<T> task:
return task.Result;
}

return default(T);
}

protected virtual Task<T> GetValueFromAsyncLazy<T>(object item)
{
switch (item)
{
case AsyncLazy<T> asyncLazy:
return asyncLazy.Value;
case Task<T> task:
return task;
// this is sync to async and only happens if you cache something sync and then get it later async
case Lazy<T> lazy:
return Task.FromResult(lazy.Value);
case T variable:
return Task.FromResult(variable);
}

return Task.FromResult(default(T));
}

protected virtual void ValidateKey(string key)
{
if (key == null)
throw new ArgumentNullException(nameof(key));

if (string.IsNullOrWhiteSpace(key))
throw new ArgumentOutOfRangeException(nameof(key), "Cache keys cannot be empty or whitespace");
}
}
}
28 changes: 28 additions & 0 deletions LazyCache/IDistributedAppCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;

namespace LazyCache
{
public interface IDistributedAppCache
{
IDistributedCacheProvider DistributedCacheProvider { get; }

/// <summary>
/// Define the number of seconds to cache objects for by default
/// </summary>
DistributedCacheDefaults DefaultCachePolicy { get; }

void Add<T>(string key, T item, DistributedCacheEntryOptions policy);

T Get<T>(string key);

T GetOrAdd<T>(string key, Func<DistributedCacheEntry, T> addItemFactory);

Task<T> GetAsync<T>(string key);

Task<T> GetOrAddAsync<T>(string key, Func<DistributedCacheEntry, Task<T>> addItemFactory);

void Remove(string key);
}
}
13 changes: 13 additions & 0 deletions LazyCache/IDistributedCacheProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;

public interface IDistributedCacheProvider
{
object Get(string key);
T Get<T>(string key);
void Set(string key, object item, DistributedCacheEntryOptions policy);
object GetOrCreate<T>(string key, Func<DistributedCacheEntry, T> func);
Task<T> GetOrCreateAsync<T>(string key, Func<DistributedCacheEntry, Task<T>> func);
void Remove(string key);
}
4 changes: 4 additions & 0 deletions LazyCache/LazyCache.csproj
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<LazyCacheVersion Condition="'$(LazyCacheVersion)' == ''">1.0.0</LazyCacheVersion>
@@ -25,6 +26,9 @@
<ItemGroup>
<PackageReference Include="microsoft.extensions.caching.abstractions" Version="2.2.0" />
<PackageReference Include="microsoft.extensions.caching.memory" Version="2.2.0" />
<PackageReference Include="MongoDB.Bson" Version="2.8.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="Newtonsoft.Json.Bson" Version="1.0.2" />
</ItemGroup>

</Project>
129 changes: 129 additions & 0 deletions LazyCache/Providers/DistributedCacheProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using System;
using System.IO;
using System.Threading.Tasks;


namespace LazyCache.Providers
{
public class DistributedCacheProvider : IDistributedCacheProvider
{
internal readonly IDistributedCache cache;

internal readonly JsonSerializerSettings deserializerSettings = new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
PreserveReferencesHandling = PreserveReferencesHandling.All,
TypeNameHandling = TypeNameHandling.All
};

public DistributedCacheProvider(IDistributedCache cache)
{
this.cache = cache;
}

internal object Set(DistributedCacheEntry entry)
{
cache.SetString(entry.Key, JsonConvert.SerializeObject(entry.Value, deserializerSettings), entry.DistributedCacheEntryOptions);
return entry.Value;
}

internal async Task SetAsync(DistributedCacheEntry entry)
{
await cache.SetStringAsync(entry.Key, JsonConvert.SerializeObject(entry.Value, deserializerSettings), entry.DistributedCacheEntryOptions);
}

public void Set(string key, object item, DistributedCacheEntryOptions policy)
{
cache.SetString(key, JsonConvert.SerializeObject(item, deserializerSettings), policy);
}

//private static string ToBson<T>(T value)
//{
// using (MemoryStream ms = new MemoryStream())
// using (BsonDataWriter datawriter = new BsonDataWriter(ms))
// {
// JsonSerializer serializer = new JsonSerializer();
// serializer.Serialize(datawriter, value);
// return Convert.ToBase64String(ms.ToArray());
// }
//}

//private static T FromBson<T>(string base64data)
//{
// byte[] data = Convert.FromBase64String(base64data);

// using (MemoryStream ms = new MemoryStream(data))
// using (BsonDataReader reader = new BsonDataReader(ms))
// {
// JsonSerializer serializer = new JsonSerializer();
// return serializer.Deserialize<T>(reader);
// }
//}

public T Get<T>(string key)
{
var cachedItem = default(T);

var valueJson = cache.GetString(key);
if (valueJson == null)
return cachedItem;
try
{
return JsonConvert.DeserializeObject<T>(valueJson, deserializerSettings);
}
catch (Exception e)
{
return cachedItem;
}
}

public object Get(string key)
{
var valueJson = cache.GetString(key);
if (valueJson == null)
return null;
return JsonConvert.DeserializeObject(valueJson, deserializerSettings);
}

public object GetOrCreate<T>(string key, Func<DistributedCacheEntry, T> func)
{
if (!TryGetValue(key, out T result))
{
var entry = new DistributedCacheEntry(key);
result = func(entry);
entry.SetValue(result);
Set(entry);
}

return result;
}

public async Task<T> GetOrCreateAsync<T>(string key, Func<DistributedCacheEntry, Task<T>> func)
{
if (!TryGetValue(key, out T result))
{
var entry = new DistributedCacheEntry(key);
result = func(entry).GetAwaiter().GetResult();
entry.SetValue(result);

await SetAsync(entry);
}

return result;
}

public void Remove(string key)
{
cache.Remove(key);
}

private bool TryGetValue<T>(string key, out T value)
{
value = Get<T>(key);
return value != null && !value.Equals(default(T));
}
}
}
70 changes: 70 additions & 0 deletions LazyCache/Providers/HybridCacheProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using System;
using System.IO;
using System.Threading.Tasks;


namespace LazyCache.Providers
{
public class HybridCacheProvider : IDistributedCacheProvider
{
private readonly IDistributedCacheProvider distributedCacheProvider;
private readonly IMemoryCache memoryCache;

public HybridCacheProvider(IDistributedCacheProvider distributedCacheProvider, IMemoryCache memoryCache)
{
this.distributedCacheProvider = distributedCacheProvider;
this.memoryCache = memoryCache;
}

public void Set(string key, object item, DistributedCacheEntryOptions policy)
{
distributedCacheProvider.Set(key, item, policy);
}

public T Get<T>(string key)
{
return distributedCacheProvider.Get<T>(key);
}

public object Get(string key)
{
return distributedCacheProvider.Get(key);
}

public object GetOrCreate<T>(string key, Func<DistributedCacheEntry, T> func)
{
if (!TryGetValue(key, out T result))
{
return memoryCache.GetOrCreate(key, (e) => func(new DistributedCacheEntry(key)));
}

return result;
}

public async Task<T> GetOrCreateAsync<T>(string key, Func<DistributedCacheEntry, Task<T>> func)
{
if (!TryGetValue(key, out T result))
{
return await memoryCache.GetOrCreateAsync(key, (e) => func(new DistributedCacheEntry(key)));
}

return result;
}

public void Remove(string key)
{
distributedCacheProvider.Remove(key);
memoryCache.Remove(key);
}

private bool TryGetValue<T>(string key, out T value)
{
value = Get<T>(key);
return value != null && !value.Equals(default(T));
}
}
}