Skip to content

Commit

Permalink
Upgrade caching to SDK 3.x
Browse files Browse the repository at this point in the history
Motivation
----------
Provide support for distributed cache to users of the 3.x SDK.

Modifications
-------------
Upgrade to SDK 3.x. Rewrite Couchbase to be based on an
ICouchbaseCollection instead of an IBucket. Also update to work with
only async methods from ICouchbaseCollection, and to specifically force
the use of the LegacyTranscoder for byte arrays.

Update the DI system to allow injecting a specific collection, but by
default inject the default collection from the bucket specified. Also
drop the overload that accepts a bucket password.

Upgrade to C# 8 and enable nullable reference types.

Update unit tests, the example project, and documentation.

Results
-------
SDK 3.x compatibility, with future-proofing for scopes/collections.

The `.GetAwaiter().GetResult()` approach to async from sync is not
perfect, as it could cause deadlocks depending on the
SynchronizationContext in use. However, it's a decent and quick approach
for now that may be improved in the future.

Closes #84
  • Loading branch information
brantburnett committed Sep 14, 2020
1 parent 931c7ea commit 2409ae3
Show file tree
Hide file tree
Showing 21 changed files with 411 additions and 512 deletions.
96 changes: 46 additions & 50 deletions docs/caching.md
Original file line number Diff line number Diff line change
@@ -1,73 +1,69 @@
# Couchbase Distributed Cache for ASP.NET Core #
A custom ASP.NET Core Middleware plugin for a distributed cache using Couchbase server as the backing store. Supports both Memcached (in-memory) and Couchbase (persistent) buckets.

A custom ASP.NET Core Middleware plugin for a distributed cache using Couchbase server as the backing store. Supports both Ephemeral (in-memory) and Couchbase (persistent) buckets.

## Getting Started ##
Assuming you have an [installation of Couchbase Server](https://developer.couchbase.com/documentation/server/4.5/getting-started/installing.html) and Visual Studio (examples with VSCODE forthcoming), do the following:

### Couchbase .NET Core Distributed Cache: ###
Assuming you have an [installation of Couchbase Server](https://docs.couchbase.com/server/current/introduction/intro.html) and Visual Studio (examples with VSCODE forthcoming), do the following:

### Couchbase .NET Core Distributed Cache ###

- Create a .NET Core Web Application using Visual Studio or VsCodeor CIL
- Install the package from [NuGet](https://www.nuget.org/packages/Couchbase.Extensions.Caching/) or build from source and add reference

### Setup ###

In Setup.cs add the following to the ConfigureServices method:

public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();

services.AddCouchbase(opt =>
```csharp
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();

services.AddCouchbase(opt =>
{
opt.Servers = new List<Uri>
{
opt.Servers = new List<Uri>
{
new Uri("http://10.111.150.101:8091")
};
});
new Uri("http://10.111.150.101:8091")
};
});

services.AddDistributedCouchbaseCache("default", opt => { });
}
services.AddDistributedCouchbaseCache("default", opt => { });
}
```

You can change the `localhost` hostname to wherever you are hosting your Couchbase cluster.

In your controller add a parameter for `IDistributedCache` to the constructor:

public class HomeController : Controller
{
private IDistributedCache _cache;

public HomeController(IDistributedCache cache)
{
_cache = cache;
}
}
```csharp
public class HomeController : Controller
{
private IDistributedCache _cache;

### Tear down ###

There are a couple different ways to free up the resources (TCP sockets, etc) opened by the Couchbase `ICluster` and `IBucket` used by the Distributed Session. Here is one simple way to tap into the `ApplicationStopped` cancellation token:

public void ConfigureServices(IServiceCollection services)
{
...

applicationLifetime.ApplicationStopped.Register(() =>
{
app.ApplicationServices.GetRequiredService<ICouchbaseLifetimeService>().Close();
});
}
public HomeController(IDistributedCache cache)
{
_cache = cache;
}
}
```

### Using Caching in your Controllers ###

Add the following code to HomeController:

public IActionResult Index()
{
_cache.Set("CacheTime", System.Text.Encoding.UTF8.GetBytes(DateTime.Now.ToString()));
return View();
}

public IActionResult About()
{
ViewData["Message"] = "Your application description page. "
+ System.Text.Encoding.UTF8.GetString(_cache.Get("CacheTime"));
return View();
}
```csharp
public async Task<IActionResult> Index()
{
await _cache.SetAsync("CacheTime", System.Text.Encoding.UTF8.GetBytes(DateTime.Now.ToString()));
return View();
}

public IActionResult About()
{
ViewData["Message"] = "Your application description page. "
+ System.Text.Encoding.UTF8.GetString(_cache.Get("CacheTime"));
return View();
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>Couchbase.Extensions.Caching</AssemblyName>
<PackageId>Couchbase.Extensions.Caching</PackageId>
<AssetTargetFallback>$(AssetTargetFallback);dnxcore50</AssetTargetFallback>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<Version>1.0.2</Version>
<Version>2.0.0</Version>
<Description>A custom ASP.NET Core Middleware plugin for distributed cache using Couchbase server as the backing store. Supports both Memcached (in-memory) and Couchbase (persistent) buckets.</Description>
<PackageTags>Couchbase;netcore;cache;session;caching;distributed;middleware;database;nosql;json</PackageTags>
<Copyright>Couchbase, Inc. 2018</Copyright>
Expand All @@ -17,14 +16,16 @@
<PackageIconUrl>https://raw.githubusercontent.com/couchbaselabs/Linq2Couchbase/master/Packaging/couchbase-logo.png</PackageIconUrl>
<RepositoryUrl>https://github.com/couchbaselabs/Couchbase.Extensions</RepositoryUrl>
<RepositoryType>git</RepositoryType>

<LangVersion>8</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CouchbaseNetClient" Version="2.6.0" />
<PackageReference Include="Couchbase.Extensions.DependencyInjection" Version="2.0.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="2.0.0" />
<PackageReference Include="CouchbaseNetClient" Version="3.0.5" />
<PackageReference Include="Couchbase.Extensions.DependencyInjection" Version="3.0.4.811" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="2.1.1" />
<PackageReference Include="System.ComponentModel.TypeConverter" Version="4.3.0" />
</ItemGroup>

Expand Down
85 changes: 33 additions & 52 deletions src/Couchbase.Extensions.Caching/CouchbaseCache.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Couchbase.Core;
using Couchbase.Core.IO.Transcoders;
using Couchbase.KeyValue;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;

Expand All @@ -12,8 +13,10 @@ public class CouchbaseCache : ICouchbaseCache
{
internal static readonly TimeSpan InfiniteLifetime = TimeSpan.Zero;

private readonly ITypeTranscoder _transcoder = new LegacyTranscoder();

/// <inheritdoc />
public IBucket Bucket { get; }
public ICouchbaseCacheCollectionProvider CollectionProvider { get; }

/// <summary>
/// Gets the options used by the Cache.
Expand All @@ -29,26 +32,19 @@ public class CouchbaseCache : ICouchbaseCache
/// </summary>
/// <param name="provider"></param>
/// <param name="options"></param>
public CouchbaseCache(ICouchbaseCacheBucketProvider provider, IOptions<CouchbaseCacheOptions> options)
public CouchbaseCache(ICouchbaseCacheCollectionProvider provider, IOptions<CouchbaseCacheOptions> options)
{
Options = options.Value;
Bucket = provider.GetBucket();
CollectionProvider = provider ?? throw new ArgumentNullException(nameof(provider));
}

/// <summary>
/// Gets a cache item by its key, returning null if the item does not exist within the Cache.
/// </summary>
/// <param name="key">The key to lookup the item.</param>
/// <returns>The cache item if found, otherwise null.</returns>
public byte[] Get(string key)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
var result = Bucket.Get<byte[]>(key);
return result.Value;
}
public byte[] Get(string key) =>
GetAsync(key).GetAwaiter().GetResult();

/// <summary>
/// Gets a cache item by its key asynchronously, returning null if the item does not exist within the Cache.
Expand All @@ -64,8 +60,10 @@ public byte[] Get(string key)
throw new ArgumentNullException(nameof(key));
}

var result = await Bucket.GetAsync<byte[]>(key).ConfigureAwait(false);
return result.Value;
var collection = await CollectionProvider.GetCollectionAsync().ConfigureAwait(false);
var result = await collection.GetAsync(key, new GetOptions().Transcoder(_transcoder))
.ConfigureAwait(false);
return result.ContentAs<byte[]>();
}

/// <summary>
Expand All @@ -82,7 +80,10 @@ async Task<byte[]> IDistributedCache.GetAsync(string key, CancellationToken toke
throw new ArgumentNullException(nameof(key));
}

return (await Bucket.GetAsync<byte[]>(key).ConfigureAwait(false)).Value;
var collection = await CollectionProvider.GetCollectionAsync().ConfigureAwait(false);
var result = await collection.GetAsync(key, new GetOptions().Transcoder(_transcoder))
.ConfigureAwait(false);
return result.ContentAs<byte[]>();
}

/// <summary>
Expand All @@ -91,19 +92,8 @@ async Task<byte[]> IDistributedCache.GetAsync(string key, CancellationToken toke
/// <param name="key">The key for the cache item.</param>
/// <param name="value">An array of bytes representing the item.</param>
/// <param name="options">The <see cref="DistributedCacheEntryOptions"/> for the item; note that only sliding expiration is currently supported.</param>
public void Set(string key, byte[] value, DistributedCacheEntryOptions options = null)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}

Bucket.Upsert(key, value, GetLifetime(options));
}
public void Set(string key, byte[] value, DistributedCacheEntryOptions? options = null) =>
SetAsync(key, value, options).GetAwaiter().GetResult();

/// <summary>
/// Sets a cache item using its key asynchronously. If the key exists, it will not be updated.
Expand All @@ -112,7 +102,7 @@ public void Set(string key, byte[] value, DistributedCacheEntryOptions options =
/// <param name="value">An array of bytes representing the item.</param>
/// <param name="options">The <see cref="DistributedCacheEntryOptions"/> for the item; note that only sliding expiration is currently supported.</param>
/// <param name="token">The <see cref="CancellationToken"/> for the operation.</param>
public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options,
public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions? options,
CancellationToken token = new CancellationToken())
{
token.ThrowIfCancellationRequested();
Expand All @@ -125,22 +115,17 @@ public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOption
throw new ArgumentNullException(nameof(value));
}

await Bucket.UpsertAsync(key, value, GetLifetime(options)).ConfigureAwait(false);
var collection = await CollectionProvider.GetCollectionAsync().ConfigureAwait(false);
await collection.UpsertAsync(key, value, new UpsertOptions().Transcoder(_transcoder).Expiry(GetLifetime(options)))
.ConfigureAwait(false);
}

/// <summary>
/// Refreshes or "touches" a key updating it's lifetime expiration.
/// </summary>
/// <param name="key">The key for the cache item.</param>
public void Refresh(string key)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}

Bucket.Touch(key, GetLifetime());
}
public void Refresh(string key) =>
RefreshAsync(key).GetAwaiter().GetResult();

/// <summary>
/// Refreshes or "touches" a key updating it's lifetime expiration asynchronously.
Expand All @@ -155,24 +140,19 @@ public void Refresh(string key)
throw new ArgumentNullException(nameof(key));
}

await Bucket.TouchAsync(key, GetLifetime()).ConfigureAwait(false);
var collection = await CollectionProvider.GetCollectionAsync().ConfigureAwait(false);
await collection.TouchAsync(key, GetLifetime()).ConfigureAwait(false);
}

/// <summary>
/// Removes an item from the cache by it's key.
/// </summary>
/// <param name="key">The key for the cache item.</param>
public void Remove(string key)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
public void Remove(string key) =>
RemoveAsync(key).GetAwaiter().GetResult();

Bucket.Remove(key);
}
/// <summary>
/// Removes an item from the cache by it's key asynchonously.
/// Removes an item from the cache by it's key asynchronously.
/// </summary>
/// <param name="key">The key for the cache item.</param>
/// <param name="token">The <see cref="CancellationToken"/> for the operation.</param>
Expand All @@ -184,11 +164,12 @@ public void Remove(string key)
throw new ArgumentNullException(nameof(key));
}

await Bucket.RemoveAsync(key).ConfigureAwait(false);
var collection = await CollectionProvider.GetCollectionAsync().ConfigureAwait(false);
await collection.RemoveAsync(key).ConfigureAwait(false);
}

/// <inheritdoc />
public TimeSpan GetLifetime(DistributedCacheEntryOptions options = null)
public TimeSpan GetLifetime(DistributedCacheEntryOptions? options = null)
{
return CouchbaseCacheExtensions.GetLifetime(this, options);
}
Expand Down
Loading

0 comments on commit 2409ae3

Please sign in to comment.