diff --git a/src/Spark.Web/NullCacheService.cs b/src/Castle.MonoRail.Views.Spark/NullCacheService.cs
similarity index 100%
rename from src/Spark.Web/NullCacheService.cs
rename to src/Castle.MonoRail.Views.Spark/NullCacheService.cs
diff --git a/src/Castle.MonoRail.Views.Spark/Wrappers/HybridCacheService.cs b/src/Castle.MonoRail.Views.Spark/Wrappers/HybridCacheService.cs
index f9bab1a3..a8a36ab0 100644
--- a/src/Castle.MonoRail.Views.Spark/Wrappers/HybridCacheService.cs
+++ b/src/Castle.MonoRail.Views.Spark/Wrappers/HybridCacheService.cs
@@ -32,7 +32,7 @@ public HybridCacheService(IEngineContext context)
}
else
{
- _fallbackCacheService = new DefaultCacheService(context.UnderlyingContext.Cache);
+ _fallbackCacheService = new WebCacheService(context.UnderlyingContext.Cache);
}
}
diff --git a/src/Spark.Tests/InMemoryServiceTest.cs b/src/Spark.Tests/InMemoryServiceTest.cs
new file mode 100644
index 00000000..8599b4dd
--- /dev/null
+++ b/src/Spark.Tests/InMemoryServiceTest.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Threading;
+using Microsoft.Extensions.Caching.Memory;
+using NUnit.Framework;
+
+namespace Spark.Tests
+{
+ [TestFixture]
+ public class InMemoryServiceTest
+ {
+ [Test]
+ public void TestStoreValueThenRetrieveIt()
+ {
+ var service = new InMemoryCacheService(new MemoryCache(new MemoryCacheOptions()));
+
+ var item = new { };
+
+ service.Store("identifier", CacheExpires.Empty, null, item);
+
+ var retrieved = service.Get("identifier");
+
+ Assert.AreSame(item, retrieved);
+ }
+
+ [Test]
+ public void TestStoreValueThenRetrieveItAfterAbsoluteExpiration()
+ {
+ var service = new InMemoryCacheService(new MemoryCache(new MemoryCacheOptions()));
+
+ var item = new { };
+
+ service.Store("identifier", new CacheExpires(DateTime.UtcNow.AddMilliseconds(50)), null, item);
+
+ Thread.Sleep(100);
+
+ var retrieved = service.Get("identifier");
+
+ Assert.IsNull(retrieved);
+ }
+
+ [Test]
+ public void TestStoreValueThenRetrieveItWhenExpirationSlides()
+ {
+ var service = new InMemoryCacheService(new MemoryCache(new MemoryCacheOptions()));
+
+ var item = new { };
+
+ service.Store("identifier", new CacheExpires(TimeSpan.FromMilliseconds(75)), null, item);
+
+ object retrieved;
+
+ for (var i = 0; i < 3; i++)
+ {
+ Thread.Sleep(50);
+
+ retrieved = service.Get("identifier");
+ }
+
+ retrieved = service.Get("identifier");
+
+ Assert.IsNotNull(retrieved);
+
+ Assert.AreSame(item, retrieved);
+ }
+
+ [Test]
+ public void TestStoreValueWithSignal()
+ {
+ var service = new InMemoryCacheService(new MemoryCache(new MemoryCacheOptions()));
+
+ var item = new { };
+
+ var signal = new CacheSignal();
+
+ service.Store("identifier", null, signal, item);
+
+ var retrieved = service.Get("identifier");
+
+ Assert.AreSame(item, retrieved);
+
+ signal.FireChanged();
+
+ retrieved = service.Get("identifier");
+
+ Assert.IsNull(retrieved);
+ }
+ }
+}
diff --git a/src/Spark.Web.Mvc/Extensions/ServiceCollectionExtensions.cs b/src/Spark.Web.Mvc/Extensions/ServiceCollectionExtensions.cs
index a99a77bb..fd33e2cc 100644
--- a/src/Spark.Web.Mvc/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Spark.Web.Mvc/Extensions/ServiceCollectionExtensions.cs
@@ -69,7 +69,7 @@ public static IServiceCollection AddSpark(this IServiceCollection services, ISpa
{
if (HttpContext.Current != null && HttpContext.Current.Cache != null)
{
- return new DefaultCacheService(HttpContext.Current.Cache);
+ return new WebCacheService(HttpContext.Current.Cache);
}
return null;
diff --git a/src/Spark.Web.Tests/Caching/CacheElementTester.cs b/src/Spark.Web.Tests/Caching/CacheElementTester.cs
index 8624b3c1..d51b261e 100644
--- a/src/Spark.Web.Tests/Caching/CacheElementTester.cs
+++ b/src/Spark.Web.Tests/Caching/CacheElementTester.cs
@@ -553,7 +553,6 @@ public void SignalWillExpireOutputCachingEntry()
2
"));
Assert.That(calls, Is.EqualTo(2));
-
}
}
}
diff --git a/src/Spark.Web/CacheExpires.cs b/src/Spark.Web/CacheExpires.cs
deleted file mode 100644
index ba91cbca..00000000
--- a/src/Spark.Web/CacheExpires.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System;
-
-namespace Spark
-{
- public class CacheExpires
- {
- private static CacheExpires _empty = new CacheExpires();
-
- public CacheExpires()
- {
- Absolute = NoAbsoluteExpiration;
- Sliding = NoSlidingExpiration;
- }
-
- public CacheExpires(DateTime absolute)
- {
- Absolute = absolute;
- Sliding = NoSlidingExpiration;
- }
-
- public CacheExpires(TimeSpan sliding)
- {
- Absolute = NoAbsoluteExpiration;
- Sliding = sliding;
- }
-
- public CacheExpires(double sliding)
- : this(TimeSpan.FromSeconds(sliding))
- {
- }
-
- public DateTime Absolute { get; set; }
- public TimeSpan Sliding { get; set; }
-
- public static DateTime NoAbsoluteExpiration { get { return System.Web.Caching.Cache.NoAbsoluteExpiration; } }
- public static TimeSpan NoSlidingExpiration { get { return System.Web.Caching.Cache.NoSlidingExpiration; } }
- public static CacheExpires Empty { get { return _empty; } }
- }
-}
\ No newline at end of file
diff --git a/src/Spark.Web/Caching/SpoolWriterOriginator.cs b/src/Spark.Web/Caching/SpoolWriterOriginator.cs
deleted file mode 100644
index f2055cfb..00000000
--- a/src/Spark.Web/Caching/SpoolWriterOriginator.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System;
-using System.Linq;
-using Spark.Spool;
-
-namespace Spark.Caching
-{
- public class SpoolWriterOriginator : TextWriterOriginator
- {
- private readonly SpoolWriter _writer;
- private int _priorStringCount;
-
- public SpoolWriterOriginator(SpoolWriter writer)
- {
- _writer = writer;
- }
-
- public override TextWriterMemento CreateMemento()
- {
- return new TextWriterMemento { Written = _writer.ToArray() };
- }
-
- public override void BeginMemento()
- {
- _priorStringCount = _writer.Count();
- }
-
- public override TextWriterMemento EndMemento()
- {
- return new TextWriterMemento { Written = _writer.Skip(_priorStringCount).ToArray() };
- }
-
- public override void DoMemento(TextWriterMemento memento)
- {
- foreach (var written in memento.Written)
- _writer.Write(written);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Spark.Web/Caching/StringWriterOriginator.cs b/src/Spark.Web/Caching/StringWriterOriginator.cs
deleted file mode 100644
index 421d0ce3..00000000
--- a/src/Spark.Web/Caching/StringWriterOriginator.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System;
-using System.IO;
-
-namespace Spark.Caching
-{
- public class StringWriterOriginator : TextWriterOriginator
- {
- private readonly StringWriter _writer;
- private int _priorLength;
-
- public StringWriterOriginator(StringWriter writer)
- {
- _writer = writer;
- }
-
- public override TextWriterMemento CreateMemento()
- {
- return new TextWriterMemento {Written = new[] {_writer.ToString()}};
- }
-
- public override void BeginMemento()
- {
- _priorLength = _writer.GetStringBuilder().Length;
- }
-
- public override TextWriterMemento EndMemento()
- {
- var currentLength = _writer.GetStringBuilder().Length;
- var written = _writer.GetStringBuilder().ToString(_priorLength, currentLength - _priorLength);
- return new TextWriterMemento { Written = new[] { written } };
- }
-
- public override void DoMemento(TextWriterMemento memento)
- {
- foreach(var written in memento.Written)
- _writer.Write(written);
- }
- }
-}
diff --git a/src/Spark.Web/Spark.Web.csproj b/src/Spark.Web/Spark.Web.csproj
index d76470b1..136e0f29 100644
--- a/src/Spark.Web/Spark.Web.csproj
+++ b/src/Spark.Web/Spark.Web.csproj
@@ -28,9 +28,6 @@
-
-
-
diff --git a/src/Spark.Web/Caching/DefaultCacheService.cs b/src/Spark.Web/WebCacheService.cs
similarity index 83%
rename from src/Spark.Web/Caching/DefaultCacheService.cs
rename to src/Spark.Web/WebCacheService.cs
index 2a1a4785..bf31bc1f 100644
--- a/src/Spark.Web/Caching/DefaultCacheService.cs
+++ b/src/Spark.Web/WebCacheService.cs
@@ -1,25 +1,25 @@
using System;
using System.Web.Caching;
-namespace Spark.Caching
+namespace Spark
{
- public class DefaultCacheService : ICacheService
+ public class WebCacheService : ICacheService
{
- private readonly Cache _cache;
+ private readonly Cache cache;
- public DefaultCacheService(Cache cache)
+ public WebCacheService(Cache cache)
{
- _cache = cache;
+ this.cache = cache;
}
public object Get(string identifier)
{
- return _cache.Get(identifier);
+ return this.cache.Get(identifier);
}
public void Store(string identifier, CacheExpires expires, ICacheSignal signal, object item)
{
- _cache.Insert(
+ this.cache.Insert(
identifier,
item,
SignalDependency.For(signal),
diff --git a/src/Spark.Web/AbstractSparkView.cs b/src/Spark/AbstractSparkView.cs
similarity index 100%
rename from src/Spark.Web/AbstractSparkView.cs
rename to src/Spark/AbstractSparkView.cs
diff --git a/src/Spark/CacheExpires.cs b/src/Spark/CacheExpires.cs
new file mode 100644
index 00000000..a9ed50cb
--- /dev/null
+++ b/src/Spark/CacheExpires.cs
@@ -0,0 +1,60 @@
+using System;
+
+namespace Spark
+{
+ ///
+ /// Represents when a cached entry should expire.
+ ///
+ public class CacheExpires
+ {
+ ///
+ /// Constructor for a non expiring cached entry.
+ ///
+ public CacheExpires()
+ {
+ Absolute = NoAbsoluteExpiration;
+ Sliding = NoSlidingExpiration;
+ }
+
+ ///
+ /// Constructor for a non cached entry expiring at a specified time.
+ ///
+ /// The time when to invalidate the cached entry.
+ public CacheExpires(DateTime absolute)
+ {
+ Absolute = absolute;
+ Sliding = NoSlidingExpiration;
+ }
+
+ ///
+ /// Constructor for a cached entry that stays cached as long as it keeps being used.
+ ///
+ /// The timespan of sliding expirations.
+ public CacheExpires(TimeSpan sliding)
+ {
+ Absolute = NoAbsoluteExpiration;
+ Sliding = sliding;
+ }
+
+ ///
+ /// Constructor for a cached entry that stays cached as long as it keeps being used.
+ ///
+ /// The number of seconds of sliding expirations.
+ public CacheExpires(double sliding)
+ : this(TimeSpan.FromSeconds(sliding))
+ {
+ }
+
+ public DateTime Absolute { get; set; }
+ public TimeSpan Sliding { get; set; }
+
+ public static DateTime NoAbsoluteExpiration => DateTime.MaxValue;
+
+ public static TimeSpan NoSlidingExpiration => TimeSpan.Zero;
+
+ ///
+ /// Cached entry never to expire.
+ ///
+ public static CacheExpires Empty { get; } = new();
+ }
+}
\ No newline at end of file
diff --git a/src/Spark.Web/CacheSignal.cs b/src/Spark/CacheSignal.cs
similarity index 92%
rename from src/Spark.Web/CacheSignal.cs
rename to src/Spark/CacheSignal.cs
index 5e00d3ab..f4db2bd5 100644
--- a/src/Spark.Web/CacheSignal.cs
+++ b/src/Spark/CacheSignal.cs
@@ -19,7 +19,7 @@ public event EventHandler Changed
lock (this)
{
_changed += value;
- if (_enabled)
+ if (_enabled)
return;
Enable();
@@ -31,7 +31,7 @@ public event EventHandler Changed
lock (this)
{
_changed -= value;
- if (_enabled != true || ChangedIsEmpty() == false)
+ if (_enabled != true || ChangedIsEmpty() == false)
return;
Disable();
@@ -42,7 +42,7 @@ public event EventHandler Changed
private bool ChangedIsEmpty()
{
- return _changed == null ||
+ return _changed == null ||
_changed.GetInvocationList().Length == 0;
}
@@ -52,7 +52,7 @@ private bool ChangedIsEmpty()
/// to call FireChanged.
///
protected virtual void Enable()
- {
+ {
}
///
@@ -60,7 +60,7 @@ protected virtual void Enable()
/// when no cache dependencies remain listenning to the signal.
///
protected virtual void Disable()
- {
+ {
}
///
@@ -69,7 +69,7 @@ protected virtual void Disable()
///
public void FireChanged()
{
- if (_changed != null)
+ if (_changed != null)
_changed(this, EventArgs.Empty);
}
}
diff --git a/src/Spark.Web/Caching/CacheMemento.cs b/src/Spark/Caching/CacheMemento.cs
similarity index 60%
rename from src/Spark.Web/Caching/CacheMemento.cs
rename to src/Spark/Caching/CacheMemento.cs
index 18c92a74..4e475e0f 100644
--- a/src/Spark.Web/Caching/CacheMemento.cs
+++ b/src/Spark/Caching/CacheMemento.cs
@@ -1,4 +1,3 @@
-using System;
using System.Collections.Generic;
using Spark.Spool;
@@ -6,14 +5,10 @@ namespace Spark.Caching
{
public class CacheMemento
{
- public CacheMemento()
- {
- Content = new Dictionary();
- OnceTable = new Dictionary();
- }
-
public SpoolWriter SpoolOutput { get; set; }
- public Dictionary Content { get; set;}
- public Dictionary OnceTable { get; set; }
+
+ public Dictionary Content { get; set; } = new();
+
+ public Dictionary OnceTable { get; set; } = new();
}
}
diff --git a/src/Spark.Web/Caching/CacheOriginator.cs b/src/Spark/Caching/CacheOriginator.cs
similarity index 71%
rename from src/Spark.Web/Caching/CacheOriginator.cs
rename to src/Spark/Caching/CacheOriginator.cs
index c0b1511d..93aa3d26 100644
--- a/src/Spark.Web/Caching/CacheOriginator.cs
+++ b/src/Spark/Caching/CacheOriginator.cs
@@ -5,42 +5,35 @@
namespace Spark.Caching
{
- public class CacheOriginator
+ public class CacheOriginator(SparkViewContext state)
{
- private readonly SparkViewContext _state;
-
private TextWriter _priorOutput;
private SpoolWriter _spoolOutput;
- private readonly Dictionary _priorContent = new Dictionary();
+ private readonly Dictionary _priorContent = new();
private Dictionary _priorOnceTable;
- public CacheOriginator(SparkViewContext state)
- {
- _state = state;
- }
-
///
/// Establishes original state for memento capturing purposes
///
public void BeginMemento()
{
- foreach (var content in _state.Content)
+ foreach (var content in state.Content)
{
var writerOriginator = TextWriterOriginator.Create(content.Value);
_priorContent.Add(content.Key, writerOriginator);
writerOriginator.BeginMemento();
}
- _priorOnceTable = _state.OnceTable.ToDictionary(kv=>kv.Key, kv=>kv.Value);
+ _priorOnceTable = state.OnceTable.ToDictionary(kv=>kv.Key, kv=>kv.Value);
// capture current output also if it's not locked into a named output at the moment
// this could be a case in view's output, direct to network, or various macro or content captures
- if (_state.Content.Any(kv => ReferenceEquals(kv.Value, _state.Output)) == false)
+ if (state.Content.Any(kv => ReferenceEquals(kv.Value, state.Output)) == false)
{
- _priorOutput = _state.Output;
+ _priorOutput = state.Output;
_spoolOutput = new SpoolWriter();
- _state.Output = _spoolOutput;
+ state.Output = _spoolOutput;
}
}
@@ -57,7 +50,7 @@ public CacheMemento EndMemento()
if (_priorOutput != null)
{
_spoolOutput.WriteTo(_priorOutput);
- _state.Output = _priorOutput;
+ state.Output = _priorOutput;
memento.SpoolOutput = _spoolOutput;
}
@@ -69,15 +62,15 @@ public CacheMemento EndMemento()
memento.Content.Add(content.Key, textMemento);
}
- // also save any named content in it's entirety that added created after BeginMemento was called
- foreach (var content in _state.Content.Where(kv => _priorContent.ContainsKey(kv.Key) == false))
+ // also save any named content in its entirety that added created after BeginMemento was called
+ foreach (var content in state.Content.Where(kv => _priorContent.ContainsKey(kv.Key) == false))
{
var originator = TextWriterOriginator.Create(content.Value);
memento.Content.Add(content.Key, originator.CreateMemento());
}
// capture anything from the oncetable that was added after BeginMemento was called
- var newItems = _state.OnceTable.Where(once => _priorOnceTable.ContainsKey(once.Key) == false);
+ var newItems = state.OnceTable.Where(once => _priorOnceTable.ContainsKey(once.Key) == false);
memento.OnceTable = newItems.ToDictionary(once => once.Key, once => once.Value);
return memento;
}
@@ -88,16 +81,16 @@ public CacheMemento EndMemento()
/// memento captured in previous begin/end calls
public void DoMemento(CacheMemento memento)
{
- memento.SpoolOutput.WriteTo(_state.Output);
+ memento.SpoolOutput.WriteTo(state.Output);
foreach (var content in memento.Content)
{
// create named content if it doesn't exist
TextWriter writer;
- if (_state.Content.TryGetValue(content.Key, out writer) == false)
+ if (state.Content.TryGetValue(content.Key, out writer) == false)
{
writer = new SpoolWriter();
- _state.Content.Add(content.Key, writer);
+ state.Content.Add(content.Key, writer);
}
// and in any case apply the delta
@@ -106,10 +99,10 @@ public void DoMemento(CacheMemento memento)
}
// add recorded once deltas that were not yet in this subject's table
- var newItems = memento.OnceTable.Where(once => _state.OnceTable.ContainsKey(once.Key) == false);
+ var newItems = memento.OnceTable.Where(once => state.OnceTable.ContainsKey(once.Key) == false);
foreach (var once in newItems)
{
- _state.OnceTable.Add(once.Key, once.Value);
+ state.OnceTable.Add(once.Key, once.Value);
}
}
}
diff --git a/src/Spark/Caching/SpoolWriterOriginator.cs b/src/Spark/Caching/SpoolWriterOriginator.cs
new file mode 100644
index 00000000..dd044591
--- /dev/null
+++ b/src/Spark/Caching/SpoolWriterOriginator.cs
@@ -0,0 +1,31 @@
+using System.Linq;
+using Spark.Spool;
+
+namespace Spark.Caching
+{
+ public class SpoolWriterOriginator(SpoolWriter writer) : TextWriterOriginator
+ {
+ private int _priorStringCount;
+
+ public override TextWriterMemento CreateMemento()
+ {
+ return new TextWriterMemento { Written = writer.ToArray() };
+ }
+
+ public override void BeginMemento()
+ {
+ _priorStringCount = writer.Count();
+ }
+
+ public override TextWriterMemento EndMemento()
+ {
+ return new TextWriterMemento { Written = writer.Skip(_priorStringCount).ToArray() };
+ }
+
+ public override void DoMemento(TextWriterMemento memento)
+ {
+ foreach (var written in memento.Written)
+ writer.Write(written);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Spark/Caching/StringWriterOriginator.cs b/src/Spark/Caching/StringWriterOriginator.cs
new file mode 100644
index 00000000..619a7ace
--- /dev/null
+++ b/src/Spark/Caching/StringWriterOriginator.cs
@@ -0,0 +1,32 @@
+using System.IO;
+
+namespace Spark.Caching
+{
+ public class StringWriterOriginator(StringWriter writer) : TextWriterOriginator
+ {
+ private int _priorLength;
+
+ public override TextWriterMemento CreateMemento()
+ {
+ return new TextWriterMemento {Written = new[] {writer.ToString()}};
+ }
+
+ public override void BeginMemento()
+ {
+ _priorLength = writer.GetStringBuilder().Length;
+ }
+
+ public override TextWriterMemento EndMemento()
+ {
+ var currentLength = writer.GetStringBuilder().Length;
+ var written = writer.GetStringBuilder().ToString(_priorLength, currentLength - _priorLength);
+ return new TextWriterMemento { Written = new[] { written } };
+ }
+
+ public override void DoMemento(TextWriterMemento memento)
+ {
+ foreach(var written in memento.Written)
+ writer.Write(written);
+ }
+ }
+}
diff --git a/src/Spark.Web/Caching/TextWriterOriginator.cs b/src/Spark/Caching/TextWriterOriginator.cs
similarity index 94%
rename from src/Spark.Web/Caching/TextWriterOriginator.cs
rename to src/Spark/Caching/TextWriterOriginator.cs
index b9885705..9727df17 100644
--- a/src/Spark.Web/Caching/TextWriterOriginator.cs
+++ b/src/Spark/Caching/TextWriterOriginator.cs
@@ -10,9 +10,15 @@ public abstract class TextWriterOriginator
public static TextWriterOriginator Create(TextWriter writer)
{
if (writer is SpoolWriter)
+ {
return new SpoolWriterOriginator((SpoolWriter) writer);
+ }
+
if (writer is StringWriter)
+ {
return new StringWriterOriginator((StringWriter)writer);
+ }
+
throw new InvalidCastException("writer is unknown type " + writer.GetType().FullName);
}
diff --git a/src/Spark/Compiler/CSharp/ChunkVisitors/GeneratedCodeVisitor.cs b/src/Spark/Compiler/CSharp/ChunkVisitors/GeneratedCodeVisitor.cs
index bff1a342..207a1641 100644
--- a/src/Spark/Compiler/CSharp/ChunkVisitors/GeneratedCodeVisitor.cs
+++ b/src/Spark/Compiler/CSharp/ChunkVisitors/GeneratedCodeVisitor.cs
@@ -503,6 +503,7 @@ protected override void Visit(CacheChunk chunk)
.RemoveIndent().WriteLine("}")
.RemoveIndent().WriteLine("}");
}
+
protected override void Visit(MarkdownChunk chunk)
{
CodeIndent(chunk).WriteLine("using(MarkdownOutputScope())");
diff --git a/src/Spark/Compiler/ChunkVisitors/DetectCodeExpressionVisitor.cs b/src/Spark/Compiler/ChunkVisitors/DetectCodeExpressionVisitor.cs
index 95307a8d..3a00578a 100644
--- a/src/Spark/Compiler/ChunkVisitors/DetectCodeExpressionVisitor.cs
+++ b/src/Spark/Compiler/ChunkVisitors/DetectCodeExpressionVisitor.cs
@@ -12,11 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
-using System;
+
using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using Spark.Compiler.ChunkVisitors;
using Spark.Parser.Code;
namespace Spark.Compiler.ChunkVisitors
@@ -62,7 +59,7 @@ void Examine(Snippets code)
protected override void Visit(UseImportChunk chunk)
{
-
+ //no-op
}
protected override void Visit(ContentSetChunk chunk)
diff --git a/src/Spark.Web/ICacheService.cs b/src/Spark/ICacheService.cs
similarity index 93%
rename from src/Spark.Web/ICacheService.cs
rename to src/Spark/ICacheService.cs
index e4937080..2c9816aa 100644
--- a/src/Spark.Web/ICacheService.cs
+++ b/src/Spark/ICacheService.cs
@@ -1,5 +1,3 @@
-using System;
-
namespace Spark
{
public interface ICacheService
diff --git a/src/Spark/InMemoryCacheService.cs b/src/Spark/InMemoryCacheService.cs
new file mode 100644
index 00000000..b60c8d9e
--- /dev/null
+++ b/src/Spark/InMemoryCacheService.cs
@@ -0,0 +1,71 @@
+using System;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Primitives;
+
+namespace Spark;
+
+public class InMemoryCacheService(IMemoryCache cache) : ICacheService
+{
+ public object Get(string identifier)
+ {
+ return cache.Get(identifier);
+ }
+
+ public void Store(string identifier, CacheExpires expires, ICacheSignal signal, object item)
+ {
+ var option = new MemoryCacheEntryOptions();
+
+ if (expires != null)
+ {
+ if (expires.Sliding > CacheExpires.NoSlidingExpiration)
+ {
+ option.SlidingExpiration = expires.Sliding;
+ }
+ else
+ {
+ option.AbsoluteExpiration = expires.Absolute;
+ }
+ }
+
+ if (signal != null)
+ {
+ option.AddExpirationToken(new SignalChangeToken(signal));
+ }
+
+ cache.Set(identifier, item, option);
+ }
+
+ private class SignalChangeToken : IChangeToken
+ {
+ private readonly ICacheSignal signal;
+ private bool hasChanged;
+
+ public SignalChangeToken(ICacheSignal signal)
+ {
+ this.signal = signal;
+ this.signal.Changed += this.SignalOnChanged;
+ }
+
+ private void SignalOnChanged(object sender, EventArgs e)
+ {
+ this.hasChanged = true;
+ }
+
+ public bool HasChanged => this.hasChanged;
+
+ public bool ActiveChangeCallbacks => true;
+
+ public IDisposable RegisterChangeCallback(Action