An open API service indexing awesome lists of open source software.

https://github.com/alexboia/servicestack-routed-cacheclient

Now you can cache your cake and eat it too: This ServiceStack cache client router enables you to use multiple cache clients and select each based on various cache key-based rules
https://github.com/alexboia/servicestack-routed-cacheclient

cache cache-client csharp net-framework netcore netcore2 netstandard netstandard20 servicestack servicestack-cache

Last synced: about 1 year ago
JSON representation

Now you can cache your cake and eat it too: This ServiceStack cache client router enables you to use multiple cache clients and select each based on various cache key-based rules

Awesome Lists containing this project

README

          

# ServiceStack Routed Cache Client

## What

The ServiceStack Routed Cache Client is an `ICacheClient` implementation
(`ICacheClientExtended` to be more exact) that acts as a facade
to a collection of multiple other cache clients.
It has minimum overhead and does not store cache values itself, but only routes
various cache operations (read, write, flush, remove etc.) to a specific registered client,
based on an associated rule (for instance, whether the key starts with a given string).

## Why

The main issue for me was (and possibly many others) that I could not register
a different cache client for session storage (say, `OrmLiteCacheClient`),
whilst continuing to make use of the standard `MemoryCacheClient `.

## Installation

Available as a NuGet package, [here](https://www.nuget.org/packages/LVD.ServiceStack.RoutedCacheClient/).

### Via Package Manager

`Install-Package LVD.ServiceStack.RoutedCacheClient -Version 1.1.0`

### Via .NET CLI
`dotnet add package LVD.ServiceStack.RoutedCacheClient --version 1.1.0`

## How to use

### 1. Add namespace reference

`using LVD.ServiceStackRoutedCacheClient`.

### 2. Create your routed cache client instance

In this example, the default `MemoryCacheClient` provided by ServiceStack is used as a fallback client,
which means it will used to handle keys that are not matched by any of the registered rules.

```csharp
IRoutedCacheClient routedCacheClient = new DefaultRoutedCacheClient(new MemoryCacheClient());
```

### 3. Register your other cache clients

For example, registering a ServiceStack OrmLite cache client for ServiceStack sessions:

```csharp
OrmLiteCacheClient ormLiteCacheClient =
new OrmLiteCacheClient();

routedCacheClient.PushClientWithRule(new KeyStartsWithStringCacheClientRule(ormLiteCacheClient,
StringComparison.InvariantCultureIgnoreCase,
"urn:iauthsession:",
"sess:"));
```

Or simpler, using `PushServiceStackSessionCacheClient()`, which automatically registers your provided cache client
with a `KeyStartsWithStringCacheClientRule` and the above prefixes: `urn:iauthsession:` and `sess:`.

```csharp
OrmLiteCacheClient ormLiteCacheClient =
new OrmLiteCacheClient();

routedCacheClient.PushServiceStackSessionCacheClient(ormLiteCacheClient);
```

### 4. Built-in routing behaviour

### 4.1. Built-in conditions

The library provides the following built-in conditions:

- `KeyStartsWithStringCacheClientRuleCondition` - matches keys that start with a given sub-string;
- `KeyEndsWithStringCacheClientRuleCondition` - matches keys that end with a given sub-string;
- `KeyMatchesRegexpCacheClientRuleCondition` - matches keys based on a regular expression;
- `ServiceStackSessionKeyCacheClientRuleCondition` - matches keys used to store ServiceStack session data;
- `AlwaysTrueCacheClientRuleCondition` - matches any key;
- `AlwaysFalseCacheClientRuleCondition` - doesn't match anything;
- `MatchAnyRuleCondition` - allows composing conditions using `OR` boolean operator (i.e. returns true if at least one condition evaluates to true);
- `MatchAllRuleCondition` - allows composing conditions using `AND` boolean operator (i.e. returns true if all conditions evaluate to true).

### 4.2. Built-in rules

The library provides the following built-in rules:

- `GenericConditionBasedCacheClientRule` - matches keys that satisfy a given condition;
- `KeyStartsWithStringCacheClientRule` - matches keys that start with a given sub-string (uses `KeyStartsWithStringCacheClientRuleCondition`);
- `KeyEndsWithStringCacheClientRule` - matches keys that end with a given sub-string (uses `KeyEndsWithStringCacheClientRuleCondition`);
- `KeyMatchesRegexpCacheClientRule` - matches keys based on a regular expression (uses `KeyMatchesRegexpCacheClientRuleCondition`);
- `ServiceStackSessionKeyCacheClientRule` - matches keys used to store ServiceStack session data (uses `ServiceStackSessionKeyCacheClientRuleCondition`);
- `AlwaysTrueCacheClientRule` - matches any key (uses `AlwaysTrueCacheClientRuleCondition`);
- `AlwaysFalseCacheClientRule` - doesn't match anything (uses `AlwaysFalseCacheClientRuleCondition`).

### 5. Extending the routing behaviour

#### 5.1. Creating custom conditions

Creating your own conditions is as easy as implementing `IRoutedCacheClientRuleCondition`
with the single method `bool Matches ( string key )`.
This is also the recommended way of exteding the library's routing behaviour.

As an example, here is the implementation of the built-in `KeyStartsWithStringCacheClientRuleCondition` condition:

```csharp
public class KeyStartsWithStringCacheClientRuleCondition : IRoutedCacheClientRuleCondition
{
private List mTokens = new List();

private StringComparison mStringComparisonMode;

public KeyStartsWithStringCacheClientRuleCondition ( StringComparison stringComparisonMode,
params string[] tokens )
{
if ( tokens == null || tokens.Length == 0 )
throw new ArgumentNullException( nameof( tokens ) );

mTokens.AddRange( tokens );
mStringComparisonMode = stringComparisonMode;
}

public bool Matches ( string key )
{
if ( string.IsNullOrWhiteSpace( key ) )
throw new ArgumentNullException( nameof( key ) );

foreach ( string token in mTokens )
if ( key.StartsWith( token, mStringComparisonMode ) )
return true;

return false;
}

public StringComparison StringComparisonMode
=> mStringComparisonMode;
}
```

#### 5.2. Creating custom rules

Creating your own rules is as easy as
- extending the `GenericConditionBasedCacheClientRule` class and providing a condition to this base class (this is a useful shortcut to always instantiating `GenericConditionBasedCacheClientRule` with the same condition).
- or extending the `BaseCacheClientRule` class and providing an implementation for the `bool Matches ( string key )` method (should you wish to bypass using a condition altogether).

Example #1 - the implementation of the built-in `KeyStartsWithStringCacheClientRule` rule:

```csharp
public class KeyStartsWithStringCacheClientRule : GenericConditionBasedCacheClientRule
{
public KeyStartsWithStringCacheClientRule ( ICacheClient cacheClient,
KeyStartsWithStringCacheClientRuleCondition condition )
: base( cacheClient, condition )
{
return;
}

public KeyStartsWithStringCacheClientRule ( ICacheClient cacheClient,
StringComparison stringComparisonMode,
params string[] tokens )
: this( cacheClient, new KeyStartsWithStringCacheClientRuleCondition( stringComparisonMode, tokens ) )
{
return;
}
}
```

Example #2 - the implementation of the built-in `GenericConditionBasedCacheClientRule` rule:
```csharp
public class GenericConditionBasedCacheClientRule : BaseCacheClientRule
{
private IRoutedCacheClientRuleCondition mCondition;

public GenericConditionBasedCacheClientRule ( ICacheClient cacheClient, IRoutedCacheClientRuleCondition condition )
: base( cacheClient )
{
mCondition = condition
?? throw new ArgumentNullException( nameof( condition ) );
}

public override bool Matches ( string key )
{
if ( string.IsNullOrWhiteSpace( key ) )
throw new ArgumentNullException( nameof( key ) );

return mCondition.Matches( key );
}

private IRoutedCacheClientRuleCondition Condition
=> mCondition;
}
```

### 6. Dispose behaviour

The dispose behaviour of the `DefaultRoutedCacheClient`, before version 1.1.0 was:

- to automatically dispose all the registered cache clients by default (this could not be altered);
- to no throw `ObjectDisposedException` when invoking methods on diposed instances.

After 1.1.0, the behaviour is as follows:

- to automatically dispose all the registered cache clients by default (this can now be configured);
- to throw throw `ObjectDisposedException` when invoking methods on diposed instances.

To change how `DefaultRoutedCacheClient` handles various registered cache clients you may:

- use `DefaultRoutedCacheClient ( ICacheClient fallbackClient, bool autoDispose )` constructor to set
whether or not the fallback cache client should be disposed along with the `DefaultRoutedCacheClient` it is registered with;
- use `IRoutedCacheClientRule.AutoDispose` property to set whether the cache client
associated with a rule should be disposed along with the `DefaultRoutedCacheClient` it is registered with.

### 7. Sample project

A sample project has been provided, to see just how this would behave in the real world.
Head over to `LVD.ServiceStack.RoutedCacheClient.Example` for the full details, but, in short, it exposes three service endpoints:

- `/set-cache-randomly` - set some cache values using the `Service.Cache` instance;
- `/set-session-randomly` - set some session values using the `Service.SessionBag` instance;
- `/list-cache-providers-data` - list all values for each of the cache providers.

The output of `/list-cache-providers-data` looks something like:

```json
{
"cacheProvidersData": {
"fallbackCacheClient": {
"SampleCacheClientService.randomText": "ef42894b-fbb6-4971-b983-0aea9e88f10a",
"SampleCacheClientService2020-04-06 10:51.hitCountsPerMinute": "12",
"SampleCacheClientService2020-04-06 10:52.hitCountsPerMinute": "4"
},
"sessionCacheClient": {
"sess:vKQ8aph3Xdp1ro9YqBzK:SampleCacheClientService.sessionRandomText": "e2f581da-ad0c-4896-8b4f-b3646f79bc7e",
"sess:vKQ8aph3Xdp1ro9YqBzK:SampleCacheClientService.hitCountsPerSession": "12"
}
}
}
```

## Breaking changes

### In Version 1.1.0

- `KeyStartsWithStringCacheClientRule` no longer extends `BaseCacheClientRule`, but `GenericConditionBasedCacheClientRule`;
- `ServiceStackSessionKeyCacheClientRule` no longer extends `BaseCacheClientRule`, but `GenericConditionBasedCacheClientRule`;
- `AlwaysTrueCacheClientRule` no longer extends `BaseCacheClientRule`, but `GenericConditionBasedCacheClientRule`;
- `AlwaysTrueCacheClientRule` now throws an exception when checking for an empty key (calling `.Matches(null|string.Empty)`);
- Calling any method on a disposed `DefaultRoutedCacheClient` instance throws `ObjectDisposedException`.

## Changelog

### Version 1.1.0

- Added utility methods to inspect cache client configuration - [Issue #4](https://github.com/alexboia/ServiceStack-Routed-CacheClient/issues/4);
- Refactoring - extracted the explicit concept of condition from that of a cache client rule - [Issue #2](https://github.com/alexboia/ServiceStack-Routed-CacheClient/issues/2);
- Additional conditions provided:
- cache key ends with substring - [Issue #5](https://github.com/alexboia/ServiceStack-Routed-CacheClient/issues/5);
- cache key matches a given regexp - [Issue #6](https://github.com/alexboia/ServiceStack-Routed-CacheClient/issues/6);
- compose conditions using OR operator;
- compose conditions using AND operator.
- Improved test coverage and code comments.

### Version 1.0.0

Initial release.

## What's next

As a rough timeline, I would like to see the following happening:

- Refactor things a bit and add some comments, at least for critical areas;
- Add some more routing rules. Here are some of the rules I'm thinking about:
- ~~match a regex pattern~~;
- ~~match rules that ends with a given string~~;
- ~~composed rules using basic `AND`/`OR` logical operators~~.
- Enhance the API:
- ~~would be useful to have a way of listing all the registered cache clients~~;
- ~~allow method chaining when registering cache client rules~~.
- Additional automated tests (in-progress).

## Donate

I put some of my free time into developing and maintaining this plugin.
If helped you in your projects and you are happy with it, you can...

[![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Q5Q01KGLM)