Skip to content

Commit 3d1eea5

Browse files
authored
More dependency injection improvements (#208)
* 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
1 parent c62806c commit 3d1eea5

4 files changed

Lines changed: 362 additions & 76 deletions

File tree

README.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ It will be retrieved from the service provider every time a cache refresh is req
265265
Create and configure your `CacheStack`, this is the backbone for Cache Tower.
266266

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

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

367367
|ℹ Need a custom context resolving solution? |
368368
|:-|
369-
|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)|
369+
|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)|
370+
371+
## <a id="named-cache-stacks"> 🏷 Named Cache Stacks
372+
373+
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.
374+
Cache Tower supports named `CacheStack` implementations via `ICacheStackAccessor`/`ICacheStackAccessor<MyContext>`.
375+
376+
This follows a similar pattern to how `IHttpClientFactory` works, allowing you to fetch the specific `CacheStack` implementation you want within your own class.
377+
378+
```csharp
379+
services.AddCacheStack<MyContext>("MyAwesomeCacheStack", (provider, builder) => builder
380+
.AddMemoryCacheLayer()
381+
.WithCleanupFrequency(TimeSpan.FromMinutes(5))
382+
);
383+
384+
public class MyController
385+
{
386+
private readonly ICacheStack<MyContext> cacheStack;
387+
388+
public MyController(ICacheStackAccessor<MyContext> cacheStackAccessor)
389+
{
390+
cacheStack = cacheStackAccessor.GetCacheStack("MyAwesomeCacheStack");
391+
}
392+
}
393+
```
370394

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

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
6+
namespace CacheTower;
7+
8+
/// <summary>
9+
/// Provides access to a named implementation of <see cref="ICacheStack"/>.
10+
/// </summary>
11+
public interface ICacheStackAccessor
12+
{
13+
/// <summary>
14+
/// Creates or returns existing named <see cref="ICacheStack"/> base on the configured builder.
15+
/// </summary>
16+
/// <param name="name">The name of the <see cref="ICacheStack"/> that has been configured.</param>
17+
/// <returns></returns>
18+
ICacheStack GetCacheStack(string name);
19+
}
20+
21+
/// <summary>
22+
/// Provides access to a named implementation of <see cref="ICacheStack{TContext}"/>.
23+
/// </summary>
24+
/// <typeparam name="TContext">The type of context that is passed during the cache entry generation process.</typeparam>
25+
public interface ICacheStackAccessor<TContext>
26+
{
27+
/// <summary>
28+
/// Creates or returns existing named <see cref="ICacheStack{TContext}"/> base on the configured builder.
29+
/// </summary>
30+
/// <param name="name">The name of the <see cref="ICacheStack{TContext}"/> that has been configured.</param>
31+
/// <returns></returns>
32+
ICacheStack<TContext> GetCacheStack(string name);
33+
}
34+
35+
internal record NamedCacheStackProvider(string Name, Func<IServiceProvider, ICacheStack> Provider);
36+
internal class NamedCacheStackLookup
37+
{
38+
private readonly ConcurrentDictionary<string, Lazy<ICacheStack>> cachedDependencies = new(StringComparer.Ordinal);
39+
private readonly Dictionary<string, NamedCacheStackProvider> namedProviders;
40+
private readonly IServiceProvider serviceProvider;
41+
42+
public NamedCacheStackLookup(
43+
IServiceProvider serviceProvider,
44+
IEnumerable<NamedCacheStackProvider> namedProviders
45+
)
46+
{
47+
this.serviceProvider = serviceProvider;
48+
this.namedProviders = namedProviders.ToDictionary(p => p.Name);
49+
}
50+
51+
public ICacheStack GetCacheStack(string name)
52+
{
53+
if (!namedProviders.TryGetValue(name, out var dependencyProvider))
54+
{
55+
throw new ArgumentException($"No ICacheStack is registered with the name \"{name}\"");
56+
}
57+
58+
return cachedDependencies.GetOrAdd(name, name => new Lazy<ICacheStack>(() => dependencyProvider.Provider(serviceProvider))).Value;
59+
}
60+
}
61+
62+
internal class CacheStackAccessor : ICacheStackAccessor
63+
{
64+
private readonly NamedCacheStackLookup cacheStackAccessor;
65+
66+
public CacheStackAccessor(NamedCacheStackLookup cacheStackAccessor)
67+
{
68+
this.cacheStackAccessor = cacheStackAccessor;
69+
}
70+
71+
public ICacheStack GetCacheStack(string name) => cacheStackAccessor.GetCacheStack(name);
72+
}
73+
74+
internal class CacheStackAccessor<TContext> : ICacheStackAccessor<TContext>
75+
{
76+
private readonly NamedCacheStackLookup cacheStackAccessor;
77+
78+
public CacheStackAccessor(NamedCacheStackLookup cacheStackAccessor)
79+
{
80+
this.cacheStackAccessor = cacheStackAccessor;
81+
}
82+
83+
public ICacheStack<TContext> GetCacheStack(string name)
84+
{
85+
if (cacheStackAccessor.GetCacheStack(name) is not ICacheStack<TContext> cacheStack)
86+
{
87+
throw new InvalidOperationException($"Registered ICacheStack for \"{name}\" is not compatible with {typeof(ICacheStack<TContext>)}");
88+
}
89+
return cacheStack;
90+
}
91+
}

src/CacheTower/ServiceCollectionExtensions.cs

Lines changed: 112 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.ComponentModel;
34
using System.Linq;
45
using CacheTower;
56
using CacheTower.Extensions;
67
using CacheTower.Providers.FileSystem;
78
using CacheTower.Providers.Memory;
9+
using Microsoft.Extensions.DependencyInjection.Extensions;
810

911
namespace Microsoft.Extensions.DependencyInjection;
1012

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

30-
internal sealed class CacheStackBuilder : ICacheStackBuilder
32+
/// <inheritdoc/>
33+
/// <typeparam name="TContext">The type of context that is passed during the cache entry generation process.</typeparam>
34+
public interface ICacheStackBuilder<TContext> : ICacheStackBuilder
35+
{
36+
/// <summary>
37+
/// The activator that is used to resolve <typeparamref name="TContext"/> for the cache entry generation process.
38+
/// </summary>
39+
/// <remarks>
40+
/// The default activator uses the current service collection as a means to instantiate <typeparamref name="TContext"/>.
41+
/// </remarks>
42+
public ICacheContextActivator CacheContextActivator { get; set; }
43+
}
44+
45+
internal class CacheStackBuilder : ICacheStackBuilder
3146
{
3247
/// <inheritdoc/>
3348
public IList<ICacheLayer> CacheLayers { get; } = new List<ICacheLayer>();
3449
/// <inheritdoc/>
3550
public IList<ICacheExtension> Extensions { get; } = new List<ICacheExtension>();
3651
}
3752

53+
internal sealed class CacheStackBuilder<TContext> : CacheStackBuilder, ICacheStackBuilder<TContext>
54+
{
55+
/// <inheritdoc/>
56+
public ICacheContextActivator CacheContextActivator { get; set; }
57+
58+
public CacheStackBuilder(ICacheContextActivator cacheContextActivator)
59+
{
60+
CacheContextActivator = cacheContextActivator;
61+
}
62+
}
3863

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

77+
private static ICacheStack BuildCacheStack(IServiceProvider provider, Action<IServiceProvider, ICacheStackBuilder> configureBuilder)
78+
{
79+
var builder = new CacheStackBuilder();
80+
configureBuilder(provider, builder);
81+
ThrowIfInvalidBuilder(builder);
82+
return new CacheStack(
83+
builder.CacheLayers.ToArray(),
84+
builder.Extensions.ToArray()
85+
);
86+
}
87+
88+
private static ICacheStack<TContext> BuildCacheStack<TContext>(IServiceProvider provider, Action<IServiceProvider, ICacheStackBuilder<TContext>> configureBuilder)
89+
{
90+
var builder = new CacheStackBuilder<TContext>(new ServiceProviderContextActivator(provider));
91+
configureBuilder(provider, builder);
92+
ThrowIfInvalidBuilder(builder);
93+
return new CacheStack<TContext>(
94+
builder.CacheContextActivator,
95+
builder.CacheLayers.ToArray(),
96+
builder.Extensions.ToArray()
97+
);
98+
}
99+
100+
/// <inheritdoc cref="AddCacheStack(IServiceCollection, Action{IServiceProvider, ICacheStackBuilder})"/>
101+
[EditorBrowsable(EditorBrowsableState.Never)]
102+
public static void AddCacheStack(this IServiceCollection services, Action<ICacheStackBuilder> configureBuilder)
103+
{
104+
services.AddCacheStack((serviceProvider, builder) => configureBuilder(builder));
105+
}
106+
52107
/// <summary>
53108
/// Adds a <see cref="CacheStack"/> to the service collection.
54109
/// </summary>
55110
/// <param name="services"></param>
56111
/// <param name="configureBuilder">The builder to configure the <see cref="CacheStack"/>.</param>
57-
public static void AddCacheStack(this IServiceCollection services, Action<ICacheStackBuilder> configureBuilder)
112+
public static void AddCacheStack(this IServiceCollection services, Action<IServiceProvider, ICacheStackBuilder> configureBuilder)
58113
{
59-
var builder = new CacheStackBuilder();
60-
configureBuilder(builder);
61-
ThrowIfInvalidBuilder(builder);
62-
services.AddSingleton<ICacheStack>(sp => new CacheStack(
63-
builder.CacheLayers.ToArray(),
64-
builder.Extensions.ToArray()
65-
));
114+
services.AddSingleton(provider => BuildCacheStack(provider, configureBuilder));
115+
}
116+
117+
/// <summary>
118+
/// Adds a <see cref="ICacheStackAccessor"/> to the service collection and configures a named <see cref="CacheStack"/>.
119+
/// </summary>
120+
/// <param name="services"></param>
121+
/// <param name="name">The name of the <see cref="CacheStack"/> to configure.</param>
122+
/// <param name="configureBuilder">The builder to configure the <see cref="CacheStack"/>.</param>
123+
public static void AddCacheStack(this IServiceCollection services, string name, Action<IServiceProvider, ICacheStackBuilder> configureBuilder)
124+
{
125+
services.TryAddSingleton<NamedCacheStackLookup>();
126+
services.TryAddSingleton<ICacheStackAccessor, CacheStackAccessor>();
127+
services.AddSingleton(provider =>
128+
{
129+
return new NamedCacheStackProvider(name, provider =>
130+
{
131+
return BuildCacheStack(provider, configureBuilder);
132+
});
133+
});
134+
}
135+
136+
/// <inheritdoc cref="AddCacheStack{TContext}(IServiceCollection, Action{IServiceProvider, ICacheStackBuilder{TContext}})"/>
137+
[EditorBrowsable(EditorBrowsableState.Never)]
138+
public static void AddCacheStack<TContext>(this IServiceCollection services, Action<ICacheStackBuilder> configureBuilder)
139+
{
140+
services.AddCacheStack<TContext>((provider, builder) => configureBuilder(builder));
66141
}
67142

68143
/// <summary>
@@ -74,16 +149,29 @@ public static void AddCacheStack(this IServiceCollection services, Action<ICache
74149
/// <typeparam name="TContext"></typeparam>
75150
/// <param name="services"></param>
76151
/// <param name="configureBuilder">The builder to configure the <see cref="CacheStack"/>.</param>
77-
public static void AddCacheStack<TContext>(this IServiceCollection services, Action<ICacheStackBuilder> configureBuilder)
152+
public static void AddCacheStack<TContext>(this IServiceCollection services, Action<IServiceProvider, ICacheStackBuilder<TContext>> configureBuilder)
78153
{
79-
var builder = new CacheStackBuilder();
80-
configureBuilder(builder);
81-
ThrowIfInvalidBuilder(builder);
82-
services.AddSingleton<ICacheStack<TContext>>(sp => new CacheStack<TContext>(
83-
new ServiceProviderContextActivator(sp),
84-
builder.CacheLayers.ToArray(),
85-
builder.Extensions.ToArray()
86-
));
154+
services.AddSingleton(provider => BuildCacheStack(provider, configureBuilder));
155+
}
156+
157+
/// <summary>
158+
/// Adds a <see cref="ICacheStackAccessor{TContext}"/> to the service collection and configures a named <see cref="CacheStack{TContext}"/>.
159+
/// </summary>
160+
/// <param name="services"></param>
161+
/// <param name="name">The name of the <see cref="CacheStack"/> to configure.</param>
162+
/// <param name="configureBuilder">The builder to configure the <see cref="CacheStack"/>.</param>
163+
public static void AddCacheStack<TContext>(this IServiceCollection services, string name, Action<IServiceProvider, ICacheStackBuilder<TContext>> configureBuilder)
164+
{
165+
services.TryAddSingleton<NamedCacheStackLookup>();
166+
services.TryAddSingleton<ICacheStackAccessor, CacheStackAccessor>();
167+
services.TryAddSingleton<ICacheStackAccessor<TContext>, CacheStackAccessor<TContext>>();
168+
services.AddSingleton(provider =>
169+
{
170+
return new NamedCacheStackProvider(name, provider =>
171+
{
172+
return BuildCacheStack(provider, configureBuilder);
173+
});
174+
});
87175
}
88176

89177
/// <summary>
@@ -93,16 +181,14 @@ public static void AddCacheStack<TContext>(this IServiceCollection services, Act
93181
/// <param name="services"></param>
94182
/// <param name="contextActivator">The activator to instantiate the <typeparamref name="TContext"/> during cache refreshing.</param>
95183
/// <param name="configureBuilder">The builder to configure the <see cref="CacheStack"/>.</param>
184+
[EditorBrowsable(EditorBrowsableState.Never)]
96185
public static void AddCacheStack<TContext>(this IServiceCollection services, ICacheContextActivator contextActivator, Action<ICacheStackBuilder> configureBuilder)
97186
{
98-
var builder = new CacheStackBuilder();
99-
configureBuilder(builder);
100-
ThrowIfInvalidBuilder(builder);
101-
services.AddSingleton<ICacheStack<TContext>>(sp => new CacheStack<TContext>(
102-
contextActivator,
103-
builder.CacheLayers.ToArray(),
104-
builder.Extensions.ToArray()
105-
));
187+
services.AddSingleton(provider => BuildCacheStack<TContext>(provider, (provider, builder) =>
188+
{
189+
builder.CacheContextActivator = contextActivator;
190+
configureBuilder(builder);
191+
}));
106192
}
107193

108194
/// <summary>

0 commit comments

Comments
 (0)