Skip to content

Commit

Permalink
More dependency injection improvements (#208)
Browse files Browse the repository at this point in the history
* Support IServiceProvider in builder pattern

* Re-ordered parameters to match AddHttpClient format

* Adds core support for named cache stacks

* Hide previous implementations of builder pattern

These won't be deprecated yet because they are so new but will be hidden by editors that observe the `EditorBrowsable` attribute.

* Updated documentation

* Use ServiceCollection implementation

* Added initial tests for named cache stacks

* Bringing ICacheContextActivator into the builder

* Remove new ICacheContextActivator variations

With the idea to move them to the builder pattern, there is no point adding new overloads that are already hidden.

* Updated documentation

* Use single NamedCacheStackLookup

This technically allows a named `ICacheStack<TContext>` to be resolved from an `ICacheStackAccessor` as well as an `ICacheStackAccessor<TContext>`. For an `ICacheStack`, that will throw an exception if tried to be resolved from an `ICacheStackAccessor<TContext>`.

* Fixed up test errors
  • Loading branch information
Turnerj committed Jul 10, 2022
1 parent c62806c commit 3d1eea5
Show file tree
Hide file tree
Showing 4 changed files with 362 additions and 76 deletions.
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ It will be retrieved from the service provider every time a cache refresh is req
Create and configure your `CacheStack`, this is the backbone for Cache Tower.

```csharp
services.AddCacheStack<UserContext>(builder => builder
services.AddCacheStack<UserContext>((provider, builder) => builder
.AddMemoryCacheLayer()
.AddRedisCacheLayer(/* Your Redis Connection */, new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance))
.WithCleanupFrequency(TimeSpan.FromMinutes(5))
Expand Down Expand Up @@ -353,7 +353,7 @@ await cacheStack.GetOrSetAsync<MyCachedType>("my-cache-key", async (oldValue, co
The type of `context` is established at the time of configuring the cache stack.

```csharp
services.AddCacheStack<MyContext>(builder => builder
services.AddCacheStack<MyContext>((provider, builder) => builder
.AddMemoryCacheLayer()
.WithCleanupFrequency(TimeSpan.FromMinutes(5))
);
Expand All @@ -366,7 +366,31 @@ You can use this context to hold any of the other objects or properties you need

|ℹ Need a custom context resolving solution? |
|:-|
|You can specify your own context activator via `AddCacheStack` by implementing a custom `ICacheContextActivator`. To see a complete example, see [this integration for SimpleInjector](https://github.com/mgoodfellow/CacheTower.ContextActivators.SimpleInjector)|
|You can specify your own context activator via `builder.CacheContextActivator` by implementing a custom `ICacheContextActivator`. To see a complete example, see [this integration for SimpleInjector](https://github.com/mgoodfellow/CacheTower.ContextActivators.SimpleInjector)|

## <a id="named-cache-stacks"> 🏷 Named Cache Stacks

You might not always want a single large `CacheStack` shared between all your code - perhaps you want an in-memory cache with a Redis layer for one section and a file cache for another.
Cache Tower supports named `CacheStack` implementations via `ICacheStackAccessor`/`ICacheStackAccessor<MyContext>`.

This follows a similar pattern to how `IHttpClientFactory` works, allowing you to fetch the specific `CacheStack` implementation you want within your own class.

```csharp
services.AddCacheStack<MyContext>("MyAwesomeCacheStack", (provider, builder) => builder
.AddMemoryCacheLayer()
.WithCleanupFrequency(TimeSpan.FromMinutes(5))
);

public class MyController
{
private readonly ICacheStack<MyContext> cacheStack;

public MyController(ICacheStackAccessor<MyContext> cacheStackAccessor)
{
cacheStack = cacheStackAccessor.GetCacheStack("MyAwesomeCacheStack");
}
}
```

## <a id="extensions" /> 🏗 Cache Tower Extensions

Expand Down
91 changes: 91 additions & 0 deletions src/CacheTower/ICacheStackAccessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace CacheTower;

/// <summary>
/// Provides access to a named implementation of <see cref="ICacheStack"/>.
/// </summary>
public interface ICacheStackAccessor
{
/// <summary>
/// Creates or returns existing named <see cref="ICacheStack"/> base on the configured builder.
/// </summary>
/// <param name="name">The name of the <see cref="ICacheStack"/> that has been configured.</param>
/// <returns></returns>
ICacheStack GetCacheStack(string name);
}

/// <summary>
/// Provides access to a named implementation of <see cref="ICacheStack{TContext}"/>.
/// </summary>
/// <typeparam name="TContext">The type of context that is passed during the cache entry generation process.</typeparam>
public interface ICacheStackAccessor<TContext>
{
/// <summary>
/// Creates or returns existing named <see cref="ICacheStack{TContext}"/> base on the configured builder.
/// </summary>
/// <param name="name">The name of the <see cref="ICacheStack{TContext}"/> that has been configured.</param>
/// <returns></returns>
ICacheStack<TContext> GetCacheStack(string name);
}

internal record NamedCacheStackProvider(string Name, Func<IServiceProvider, ICacheStack> Provider);
internal class NamedCacheStackLookup
{
private readonly ConcurrentDictionary<string, Lazy<ICacheStack>> cachedDependencies = new(StringComparer.Ordinal);
private readonly Dictionary<string, NamedCacheStackProvider> namedProviders;
private readonly IServiceProvider serviceProvider;

public NamedCacheStackLookup(
IServiceProvider serviceProvider,
IEnumerable<NamedCacheStackProvider> namedProviders
)
{
this.serviceProvider = serviceProvider;
this.namedProviders = namedProviders.ToDictionary(p => p.Name);
}

public ICacheStack GetCacheStack(string name)
{
if (!namedProviders.TryGetValue(name, out var dependencyProvider))
{
throw new ArgumentException($"No ICacheStack is registered with the name \"{name}\"");
}

return cachedDependencies.GetOrAdd(name, name => new Lazy<ICacheStack>(() => dependencyProvider.Provider(serviceProvider))).Value;
}
}

internal class CacheStackAccessor : ICacheStackAccessor
{
private readonly NamedCacheStackLookup cacheStackAccessor;

public CacheStackAccessor(NamedCacheStackLookup cacheStackAccessor)
{
this.cacheStackAccessor = cacheStackAccessor;
}

public ICacheStack GetCacheStack(string name) => cacheStackAccessor.GetCacheStack(name);
}

internal class CacheStackAccessor<TContext> : ICacheStackAccessor<TContext>
{
private readonly NamedCacheStackLookup cacheStackAccessor;

public CacheStackAccessor(NamedCacheStackLookup cacheStackAccessor)
{
this.cacheStackAccessor = cacheStackAccessor;
}

public ICacheStack<TContext> GetCacheStack(string name)
{
if (cacheStackAccessor.GetCacheStack(name) is not ICacheStack<TContext> cacheStack)
{
throw new InvalidOperationException($"Registered ICacheStack for \"{name}\" is not compatible with {typeof(ICacheStack<TContext>)}");
}
return cacheStack;
}
}
138 changes: 112 additions & 26 deletions src/CacheTower/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using CacheTower;
using CacheTower.Extensions;
using CacheTower.Providers.FileSystem;
using CacheTower.Providers.Memory;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Extensions.DependencyInjection;

Expand All @@ -27,14 +29,37 @@ public interface ICacheStackBuilder
IList<ICacheExtension> Extensions { get; }
}

internal sealed class CacheStackBuilder : ICacheStackBuilder
/// <inheritdoc/>
/// <typeparam name="TContext">The type of context that is passed during the cache entry generation process.</typeparam>
public interface ICacheStackBuilder<TContext> : ICacheStackBuilder
{
/// <summary>
/// The activator that is used to resolve <typeparamref name="TContext"/> for the cache entry generation process.
/// </summary>
/// <remarks>
/// The default activator uses the current service collection as a means to instantiate <typeparamref name="TContext"/>.
/// </remarks>
public ICacheContextActivator CacheContextActivator { get; set; }
}

internal class CacheStackBuilder : ICacheStackBuilder
{
/// <inheritdoc/>
public IList<ICacheLayer> CacheLayers { get; } = new List<ICacheLayer>();
/// <inheritdoc/>
public IList<ICacheExtension> Extensions { get; } = new List<ICacheExtension>();
}

internal sealed class CacheStackBuilder<TContext> : CacheStackBuilder, ICacheStackBuilder<TContext>
{
/// <inheritdoc/>
public ICacheContextActivator CacheContextActivator { get; set; }

public CacheStackBuilder(ICacheContextActivator cacheContextActivator)
{
CacheContextActivator = cacheContextActivator;
}
}

/// <summary>
/// Microsoft <see cref="IServiceCollection"/> extensions for Cache Tower.
Expand All @@ -49,20 +74,70 @@ private static void ThrowIfInvalidBuilder(ICacheStackBuilder builder)
}
}

private static ICacheStack BuildCacheStack(IServiceProvider provider, Action<IServiceProvider, ICacheStackBuilder> configureBuilder)
{
var builder = new CacheStackBuilder();
configureBuilder(provider, builder);
ThrowIfInvalidBuilder(builder);
return new CacheStack(
builder.CacheLayers.ToArray(),
builder.Extensions.ToArray()
);
}

private static ICacheStack<TContext> BuildCacheStack<TContext>(IServiceProvider provider, Action<IServiceProvider, ICacheStackBuilder<TContext>> configureBuilder)
{
var builder = new CacheStackBuilder<TContext>(new ServiceProviderContextActivator(provider));
configureBuilder(provider, builder);
ThrowIfInvalidBuilder(builder);
return new CacheStack<TContext>(
builder.CacheContextActivator,
builder.CacheLayers.ToArray(),
builder.Extensions.ToArray()
);
}

/// <inheritdoc cref="AddCacheStack(IServiceCollection, Action{IServiceProvider, ICacheStackBuilder})"/>
[EditorBrowsable(EditorBrowsableState.Never)]
public static void AddCacheStack(this IServiceCollection services, Action<ICacheStackBuilder> configureBuilder)
{
services.AddCacheStack((serviceProvider, builder) => configureBuilder(builder));
}

/// <summary>
/// Adds a <see cref="CacheStack"/> to the service collection.
/// </summary>
/// <param name="services"></param>
/// <param name="configureBuilder">The builder to configure the <see cref="CacheStack"/>.</param>
public static void AddCacheStack(this IServiceCollection services, Action<ICacheStackBuilder> configureBuilder)
public static void AddCacheStack(this IServiceCollection services, Action<IServiceProvider, ICacheStackBuilder> configureBuilder)
{
var builder = new CacheStackBuilder();
configureBuilder(builder);
ThrowIfInvalidBuilder(builder);
services.AddSingleton<ICacheStack>(sp => new CacheStack(
builder.CacheLayers.ToArray(),
builder.Extensions.ToArray()
));
services.AddSingleton(provider => BuildCacheStack(provider, configureBuilder));
}

/// <summary>
/// Adds a <see cref="ICacheStackAccessor"/> to the service collection and configures a named <see cref="CacheStack"/>.
/// </summary>
/// <param name="services"></param>
/// <param name="name">The name of the <see cref="CacheStack"/> to configure.</param>
/// <param name="configureBuilder">The builder to configure the <see cref="CacheStack"/>.</param>
public static void AddCacheStack(this IServiceCollection services, string name, Action<IServiceProvider, ICacheStackBuilder> configureBuilder)
{
services.TryAddSingleton<NamedCacheStackLookup>();
services.TryAddSingleton<ICacheStackAccessor, CacheStackAccessor>();
services.AddSingleton(provider =>
{
return new NamedCacheStackProvider(name, provider =>
{
return BuildCacheStack(provider, configureBuilder);
});
});
}

/// <inheritdoc cref="AddCacheStack{TContext}(IServiceCollection, Action{IServiceProvider, ICacheStackBuilder{TContext}})"/>
[EditorBrowsable(EditorBrowsableState.Never)]
public static void AddCacheStack<TContext>(this IServiceCollection services, Action<ICacheStackBuilder> configureBuilder)
{
services.AddCacheStack<TContext>((provider, builder) => configureBuilder(builder));
}

/// <summary>
Expand All @@ -74,16 +149,29 @@ public static void AddCacheStack(this IServiceCollection services, Action<ICache
/// <typeparam name="TContext"></typeparam>
/// <param name="services"></param>
/// <param name="configureBuilder">The builder to configure the <see cref="CacheStack"/>.</param>
public static void AddCacheStack<TContext>(this IServiceCollection services, Action<ICacheStackBuilder> configureBuilder)
public static void AddCacheStack<TContext>(this IServiceCollection services, Action<IServiceProvider, ICacheStackBuilder<TContext>> configureBuilder)
{
var builder = new CacheStackBuilder();
configureBuilder(builder);
ThrowIfInvalidBuilder(builder);
services.AddSingleton<ICacheStack<TContext>>(sp => new CacheStack<TContext>(
new ServiceProviderContextActivator(sp),
builder.CacheLayers.ToArray(),
builder.Extensions.ToArray()
));
services.AddSingleton(provider => BuildCacheStack(provider, configureBuilder));
}

/// <summary>
/// Adds a <see cref="ICacheStackAccessor{TContext}"/> to the service collection and configures a named <see cref="CacheStack{TContext}"/>.
/// </summary>
/// <param name="services"></param>
/// <param name="name">The name of the <see cref="CacheStack"/> to configure.</param>
/// <param name="configureBuilder">The builder to configure the <see cref="CacheStack"/>.</param>
public static void AddCacheStack<TContext>(this IServiceCollection services, string name, Action<IServiceProvider, ICacheStackBuilder<TContext>> configureBuilder)
{
services.TryAddSingleton<NamedCacheStackLookup>();
services.TryAddSingleton<ICacheStackAccessor, CacheStackAccessor>();
services.TryAddSingleton<ICacheStackAccessor<TContext>, CacheStackAccessor<TContext>>();
services.AddSingleton(provider =>
{
return new NamedCacheStackProvider(name, provider =>
{
return BuildCacheStack(provider, configureBuilder);
});
});
}

/// <summary>
Expand All @@ -93,16 +181,14 @@ public static void AddCacheStack<TContext>(this IServiceCollection services, Act
/// <param name="services"></param>
/// <param name="contextActivator">The activator to instantiate the <typeparamref name="TContext"/> during cache refreshing.</param>
/// <param name="configureBuilder">The builder to configure the <see cref="CacheStack"/>.</param>
[EditorBrowsable(EditorBrowsableState.Never)]
public static void AddCacheStack<TContext>(this IServiceCollection services, ICacheContextActivator contextActivator, Action<ICacheStackBuilder> configureBuilder)
{
var builder = new CacheStackBuilder();
configureBuilder(builder);
ThrowIfInvalidBuilder(builder);
services.AddSingleton<ICacheStack<TContext>>(sp => new CacheStack<TContext>(
contextActivator,
builder.CacheLayers.ToArray(),
builder.Extensions.ToArray()
));
services.AddSingleton(provider => BuildCacheStack<TContext>(provider, (provider, builder) =>
{
builder.CacheContextActivator = contextActivator;
configureBuilder(builder);
}));
}

/// <summary>
Expand Down
Loading

0 comments on commit 3d1eea5

Please sign in to comment.