Skip to content
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

More dependency injection improvements #208

Merged
merged 12 commits into from
Jul 10, 2022
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