diff --git a/samples/Server/Startup.cs b/samples/Server/Startup.cs
index 5450fe3f..edceeb3b 100644
--- a/samples/Server/Startup.cs
+++ b/samples/Server/Startup.cs
@@ -1,4 +1,3 @@
-using Microsoft.Extensions.Options;
using MudBlazor.Markdown.Core.Utils.ServiceRegistration;
using MudBlazor.Services;
@@ -21,8 +20,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddMudServices();
services.AddMudMarkdownServices(static cache =>
{
- cache.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
- cache.SlidingExpiration = TimeSpan.FromHours(2);
+ cache.TimeToLive = TimeSpan.FromHours(2);
});
}
diff --git a/src/MudBlazor.Markdown/Models/MudMarkdownMemoryCacheEntryOptions.cs b/src/MudBlazor.Markdown/Models/MudMarkdownMemoryCacheEntryOptions.cs
deleted file mode 100644
index 0bde4f69..00000000
--- a/src/MudBlazor.Markdown/Models/MudMarkdownMemoryCacheEntryOptions.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace MudBlazor;
-
-public sealed class MudMarkdownMemoryCacheEntryOptions : MemoryCacheEntryOptions;
diff --git a/src/MudBlazor.Markdown/Models/MudMarkdownMemoryCacheOptions.cs b/src/MudBlazor.Markdown/Models/MudMarkdownMemoryCacheOptions.cs
new file mode 100644
index 00000000..75aedfb6
--- /dev/null
+++ b/src/MudBlazor.Markdown/Models/MudMarkdownMemoryCacheOptions.cs
@@ -0,0 +1,6 @@
+namespace MudBlazor;
+
+public sealed class MudMarkdownMemoryCacheOptions
+{
+ public TimeSpan TimeToLive { get; set; }
+}
diff --git a/src/MudBlazor.Markdown/MudBlazor.Markdown.csproj b/src/MudBlazor.Markdown/MudBlazor.Markdown.csproj
index b94065f2..972d0699 100644
--- a/src/MudBlazor.Markdown/MudBlazor.Markdown.csproj
+++ b/src/MudBlazor.Markdown/MudBlazor.Markdown.csproj
@@ -22,7 +22,6 @@
-
diff --git a/src/MudBlazor.Markdown/Services/Interfaces/IMudMarkdownMemoryCache.cs b/src/MudBlazor.Markdown/Services/Interfaces/IMudMarkdownMemoryCache.cs
index e06c0c8a..875275ae 100644
--- a/src/MudBlazor.Markdown/Services/Interfaces/IMudMarkdownMemoryCache.cs
+++ b/src/MudBlazor.Markdown/Services/Interfaces/IMudMarkdownMemoryCache.cs
@@ -1,3 +1,8 @@
namespace MudBlazor;
-internal interface IMudMarkdownMemoryCache : IMemoryCache;
+internal interface IMudMarkdownMemoryCache
+{
+ public bool TryGetValue(in string key, out string value);
+
+ public void Set(in string key, in string value);
+}
diff --git a/src/MudBlazor.Markdown/Services/MudMarkdownMemoryCache.cs b/src/MudBlazor.Markdown/Services/MudMarkdownMemoryCache.cs
index 228ba4c3..ba78a368 100644
--- a/src/MudBlazor.Markdown/Services/MudMarkdownMemoryCache.cs
+++ b/src/MudBlazor.Markdown/Services/MudMarkdownMemoryCache.cs
@@ -1,16 +1,88 @@
-using Microsoft.Extensions.Logging;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
namespace MudBlazor;
-internal sealed class MudMarkdownMemoryCache : MemoryCache, IMudMarkdownMemoryCache
+internal sealed class MudMarkdownMemoryCache : IMudMarkdownMemoryCache
{
- public MudMarkdownMemoryCache(IOptions optionsAccessor)
- : base(optionsAccessor)
+#if NET9_0_OR_GREATER
+ private readonly Lock _lock = new();
+#elif NET8_0
+ private readonly object _lock = new();
+#endif
+ private readonly Dictionary _memoryCache = new();
+ private readonly long _ttl;
+
+ public MudMarkdownMemoryCache(IOptions options)
+ {
+ var timespan = options.Value.TimeToLive;
+ if (timespan <= TimeSpan.Zero)
+ timespan = TimeSpan.FromHours(1);
+
+ _ttl = Convert.ToInt64(timespan.TotalSeconds);
+ }
+
+ public bool TryGetValue(in string key, out string value)
+ {
+ Entry? entry;
+ lock (_lock)
+ {
+ if (!_memoryCache.TryGetValue(key, out entry))
+ goto False;
+ }
+
+ var currentTime = CurrentUnixTimeSeconds();
+ if (entry.ExpiresAt > currentTime)
+ {
+ value = entry.Value;
+ return true;
+ }
+
+ lock (_lock)
+ {
+ _memoryCache.Remove(key);
+ }
+
+ False:
+ value = string.Empty;
+ return false;
+ }
+
+ public void Set(in string key, in string value)
{
+ lock (_lock)
+ {
+ ref var entryRef = ref CollectionsMarshal.GetValueRefOrAddDefault(_memoryCache, key, out _);
+ if (entryRef is null)
+ {
+ entryRef = new Entry(value)
+ {
+ ExpiresAt = GetExpiresAt(),
+ };
+ }
+ else
+ {
+ entryRef.ExpiresAt = GetExpiresAt();
+ }
+ }
}
- public MudMarkdownMemoryCache(IOptions optionsAccessor, ILoggerFactory loggerFactory)
- : base(optionsAccessor, loggerFactory)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private long GetExpiresAt() =>
+ CurrentUnixTimeSeconds() + _ttl;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static long CurrentUnixTimeSeconds() =>
+ DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+
+ private sealed class Entry
{
+ public readonly string Value;
+ public long ExpiresAt;
+
+ public Entry(in string value)
+ {
+ Value = value;
+ }
}
}
diff --git a/src/MudBlazor.Markdown/Services/MudMarkdownValueProvider.cs b/src/MudBlazor.Markdown/Services/MudMarkdownValueProvider.cs
index cf81bc4c..22f5cb80 100644
--- a/src/MudBlazor.Markdown/Services/MudMarkdownValueProvider.cs
+++ b/src/MudBlazor.Markdown/Services/MudMarkdownValueProvider.cs
@@ -5,13 +5,11 @@ namespace MudBlazor;
internal sealed class MudMarkdownValueProvider : IMudMarkdownValueProvider
{
private readonly IMudMarkdownMemoryCache _memoryCache;
- private readonly MemoryCacheEntryOptions _memoryCacheEntryOptions;
private static readonly HttpClient HttpClient = new();
- public MudMarkdownValueProvider(IMudMarkdownMemoryCache memoryCache, IOptions options)
+ public MudMarkdownValueProvider(IMudMarkdownMemoryCache memoryCache)
{
_memoryCache = memoryCache;
- _memoryCacheEntryOptions = options.Value;
}
public async ValueTask GetValueAsync(string value, MarkdownSourceType sourceType, CancellationToken ct = default)
@@ -27,7 +25,7 @@ public async ValueTask GetValueAsync(string value, MarkdownSourceType so
private async ValueTask ReadFromFileAsync(string path, CancellationToken ct = default)
{
- if (_memoryCache.TryGetValue(path, out var value) && value is not null)
+ if (_memoryCache.TryGetValue(path, out var value))
return value;
try
@@ -38,7 +36,7 @@ private async ValueTask ReadFromFileAsync(string path, CancellationToken
value = await reader.ReadToEndAsync(ct)
.ConfigureAwait(false);
- _memoryCache.Set(path, value, _memoryCacheEntryOptions);
+ _memoryCache.Set(path, value);
return value;
}
catch (Exception e)
@@ -51,7 +49,7 @@ private async ValueTask ReadFromFileAsync(string path, CancellationToken
private async ValueTask ReadFromUrlAsync(string url, CancellationToken ct = default)
{
- if (_memoryCache.TryGetValue(url, out var value) && value is not null)
+ if (_memoryCache.TryGetValue(url, out var value))
return value;
try
@@ -59,7 +57,7 @@ private async ValueTask ReadFromUrlAsync(string url, CancellationToken c
value = await HttpClient.GetStringAsync(url, ct)
.ConfigureAwait(false);
- _memoryCache.Set(url, value, _memoryCacheEntryOptions);
+ _memoryCache.Set(url, value);
return value;
}
catch (Exception e)
diff --git a/src/MudBlazor.Markdown/Utils/ServiceRegistration/ServiceCollectionEx.cs b/src/MudBlazor.Markdown/Utils/ServiceRegistration/ServiceCollectionEx.cs
index a118db02..eacd7fb6 100644
--- a/src/MudBlazor.Markdown/Utils/ServiceRegistration/ServiceCollectionEx.cs
+++ b/src/MudBlazor.Markdown/Utils/ServiceRegistration/ServiceCollectionEx.cs
@@ -2,7 +2,7 @@
public static class ServiceCollectionEx
{
- public static IServiceCollection AddMudMarkdownServices(this IServiceCollection @this, Action? configureMemoryCache = null)
+ public static IServiceCollection AddMudMarkdownServices(this IServiceCollection @this, Action? configureMemoryCache = null)
{
return @this
.AddMudMarkdownCache(configureMemoryCache)
@@ -10,22 +10,18 @@ public static IServiceCollection AddMudMarkdownServices(this IServiceCollection
.AddSingleton();
}
- private static IServiceCollection AddMudMarkdownCache(this IServiceCollection @this, Action? configureMemoryCache)
+ private static IServiceCollection AddMudMarkdownCache(this IServiceCollection @this, Action? configureMemoryCache)
{
return @this
.AddOptions()
- .AddSingleton()
- .Configure(options =>
+ .Configure(options =>
{
- if (configureMemoryCache != null)
- {
+ if (configureMemoryCache is not null)
configureMemoryCache(options);
- }
else
- {
- options.SlidingExpiration = TimeSpan.FromHours(1);
- }
- });
+ options.TimeToLive = TimeSpan.FromHours(1);
+ })
+ .AddSingleton();
}
public static IServiceCollection AddMudMarkdownClipboardService(this IServiceCollection @this)
diff --git a/src/MudBlazor.Markdown/_Usings.cs b/src/MudBlazor.Markdown/_Usings.cs
index 4fa7422f..c1722285 100644
--- a/src/MudBlazor.Markdown/_Usings.cs
+++ b/src/MudBlazor.Markdown/_Usings.cs
@@ -2,9 +2,7 @@
global using Markdig;
global using Markdig.Helpers;
global using Microsoft.AspNetCore.Components;
-global using Microsoft.AspNetCore.Components.CompilerServices;
global using Microsoft.AspNetCore.Components.Rendering;
-global using Microsoft.Extensions.Caching.Memory;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Options;
global using Microsoft.JSInterop;
diff --git a/tests/MudBlazor.Markdown.Tests/Assertions.cs b/tests/MudBlazor.Markdown.Tests/Assertions.cs
new file mode 100644
index 00000000..2a8c10ad
--- /dev/null
+++ b/tests/MudBlazor.Markdown.Tests/Assertions.cs
@@ -0,0 +1,72 @@
+using System.Collections;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using FluentAssertions.Primitives;
+
+namespace MudBlazor.Markdown.Tests;
+
+internal static class MudMarkdownMemoryCacheEx
+{
+#if NET9_0_OR_GREATER
+ [OverloadResolutionPriority(1)]
+#endif
+ public static MudMarkdownMemoryCacheAssertions Should(this IMudMarkdownMemoryCache? @this)
+ {
+ return new MudMarkdownMemoryCacheAssertions(@this);
+ }
+}
+
+internal sealed class MudMarkdownMemoryCacheAssertions(IMudMarkdownMemoryCache? value) : ObjectAssertions(value)
+{
+ private FieldInfo? _cacheField;
+
+ public void HaveEmptyCache()
+ {
+ var memoryCache = GetMemoryCache();
+
+ memoryCache.Count
+ .Should()
+ .Be(0);
+ }
+
+ public void HaveSingleCacheEntry(string key)
+ {
+ var memoryCache = GetMemoryCache();
+
+ memoryCache.Keys.Count
+ .Should()
+ .Be(1);
+
+ memoryCache.Keys
+ .Cast()
+ .Single()
+ .Should()
+ .Be(key);
+ }
+
+ public void HaveKeys(IReadOnlyList keys)
+ {
+ var memoryCache = GetMemoryCache();
+
+ memoryCache.Keys
+ .Should()
+ .BeEquivalentTo(keys);
+ }
+
+ private IDictionary GetMemoryCache()
+ {
+ var field = GetMemoryCacheField();
+ return (IDictionary?)field.GetValue(Subject) ?? throw new InvalidOperationException("Memory cache is null.");
+ }
+
+ private FieldInfo GetMemoryCacheField()
+ {
+ if (_cacheField is not null)
+ return _cacheField;
+
+ var cacheField = Subject?.GetType()
+ .GetField("_memoryCache", BindingFlags.Instance | BindingFlags.NonPublic);
+
+ return _cacheField = cacheField ?? throw new InvalidOperationException("Cache field not found.");
+ }
+}
diff --git a/tests/MudBlazor.Markdown.Tests/MudBlazor.Markdown.Tests.csproj b/tests/MudBlazor.Markdown.Tests/MudBlazor.Markdown.Tests.csproj
index 74c7e8fb..b6580915 100644
--- a/tests/MudBlazor.Markdown.Tests/MudBlazor.Markdown.Tests.csproj
+++ b/tests/MudBlazor.Markdown.Tests/MudBlazor.Markdown.Tests.csproj
@@ -14,7 +14,7 @@
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/tests/MudBlazor.Markdown.Tests/Services/MudMarkdownMemoryCacheTests/MudMarkdownMemoryCacheTestsBase.cs b/tests/MudBlazor.Markdown.Tests/Services/MudMarkdownMemoryCacheTests/MudMarkdownMemoryCacheTestsBase.cs
new file mode 100644
index 00000000..9bd5feef
--- /dev/null
+++ b/tests/MudBlazor.Markdown.Tests/Services/MudMarkdownMemoryCacheTests/MudMarkdownMemoryCacheTestsBase.cs
@@ -0,0 +1,27 @@
+namespace MudBlazor.Markdown.Tests.Services.MudMarkdownMemoryCacheTests;
+
+public abstract class MudMarkdownMemoryCacheTestsBase : IDisposable
+{
+ private ServiceProvider? _serviceProvider;
+
+ protected TimeSpan? TimeToLive { get; set; }
+
+ internal IMudMarkdownMemoryCache CreateFixture()
+ {
+ _serviceProvider ??= new ServiceCollection()
+ .AddMudMarkdownServices(opts =>
+ {
+ if (TimeToLive.HasValue)
+ opts.TimeToLive = TimeToLive.Value;
+ })
+ .BuildServiceProvider();
+
+ return _serviceProvider.GetRequiredService();
+ }
+
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ _serviceProvider?.Dispose();
+ }
+}
diff --git a/tests/MudBlazor.Markdown.Tests/Services/MudMarkdownMemoryCacheTests/SetShould.cs b/tests/MudBlazor.Markdown.Tests/Services/MudMarkdownMemoryCacheTests/SetShould.cs
new file mode 100644
index 00000000..6d4643ed
--- /dev/null
+++ b/tests/MudBlazor.Markdown.Tests/Services/MudMarkdownMemoryCacheTests/SetShould.cs
@@ -0,0 +1,35 @@
+namespace MudBlazor.Markdown.Tests.Services.MudMarkdownMemoryCacheTests;
+
+public sealed class SetShould : MudMarkdownMemoryCacheTestsBase
+{
+ [Fact]
+ public void SetDifferentKeys()
+ {
+ const string key1 = nameof(key1),
+ key2 = nameof(key2),
+ value = nameof(value);
+
+ var fixture = CreateFixture();
+ fixture.Set(key1, value);
+ fixture.Set(key2, value);
+
+ fixture
+ .Should()
+ .HaveKeys([key1, key2]);
+ }
+
+ [Fact]
+ public void SetSameKeys()
+ {
+ const string key = nameof(key), value = nameof(value);
+
+ var fixture = CreateFixture();
+ fixture.Set(key, value);
+ fixture.Set(key, value);
+ fixture.Set(key, value);
+
+ fixture
+ .Should()
+ .HaveSingleCacheEntry(key);
+ }
+}
diff --git a/tests/MudBlazor.Markdown.Tests/Services/MudMarkdownMemoryCacheTests/TryGetValueShould.cs b/tests/MudBlazor.Markdown.Tests/Services/MudMarkdownMemoryCacheTests/TryGetValueShould.cs
new file mode 100644
index 00000000..2cad024e
--- /dev/null
+++ b/tests/MudBlazor.Markdown.Tests/Services/MudMarkdownMemoryCacheTests/TryGetValueShould.cs
@@ -0,0 +1,98 @@
+namespace MudBlazor.Markdown.Tests.Services.MudMarkdownMemoryCacheTests;
+
+public sealed class TryGetValueShould : MudMarkdownMemoryCacheTestsBase
+{
+ [Fact]
+ public void ReturnFalseIfNotSet()
+ {
+ const string key = nameof(key);
+
+ var fixture = CreateFixture();
+ var actual = fixture.TryGetValue(key, out var actualValue);
+
+ actual
+ .Should()
+ .BeFalse();
+
+ actualValue
+ .Should()
+ .BeEmpty();
+
+ fixture
+ .Should()
+ .HaveEmptyCache();
+ }
+
+ [Fact]
+ public void ReturnTrueIfSet()
+ {
+ const string key = nameof(key), value = nameof(value);
+
+ var fixture = CreateFixture();
+ fixture.Set(key, value);
+
+ var actual = fixture.TryGetValue(key, out var actualValue);
+
+ actual
+ .Should()
+ .BeTrue();
+
+ actualValue
+ .Should()
+ .Be(value);
+
+ fixture
+ .Should()
+ .HaveSingleCacheEntry(key);
+ }
+
+ [Fact]
+ public async Task ReturnFalseAfterExpiration()
+ {
+ TimeToLive = TimeSpan.FromSeconds(1);
+ const string key = nameof(key), value = nameof(value);
+
+ var fixture = CreateFixture();
+ fixture.Set(key, value);
+
+ await Task.Delay(TimeToLive.Value);
+
+ var actual = fixture.TryGetValue(key, out var actualValue);
+
+ actual
+ .Should()
+ .BeFalse();
+
+ actualValue
+ .Should()
+ .BeEmpty();
+
+ fixture
+ .Should()
+ .HaveEmptyCache();
+ }
+
+ [Fact]
+ public async Task ReturnTrueAfterExtendedExpiresAt()
+ {
+ TimeToLive = TimeSpan.FromSeconds(2);
+ const string key = nameof(key), value = nameof(value);
+
+ var fixture = CreateFixture();
+ fixture.Set(key, value);
+ await Task.Delay(TimeSpan.FromSeconds(1));
+
+ fixture.Set(key, value);
+ await Task.Delay(TimeSpan.FromSeconds(1.01d));
+
+ var actual = fixture.TryGetValue(key, out var actualValue);
+
+ actual
+ .Should()
+ .BeTrue();
+
+ actualValue
+ .Should()
+ .Be(value);
+ }
+}
diff --git a/tests/MudBlazor.Markdown.Tests/Utils/ServiceCollectionExTests/AddMudMarkdownServicesShould.cs b/tests/MudBlazor.Markdown.Tests/Utils/ServiceCollectionExTests/AddMudMarkdownServicesShould.cs
index ed9b6205..70035894 100644
--- a/tests/MudBlazor.Markdown.Tests/Utils/ServiceCollectionExTests/AddMudMarkdownServicesShould.cs
+++ b/tests/MudBlazor.Markdown.Tests/Utils/ServiceCollectionExTests/AddMudMarkdownServicesShould.cs
@@ -42,24 +42,6 @@ public void RegisterCustomMemoryCache()
.Be(scopedInstance);
}
- [Fact]
- public void RegisterScopedMemoryCache()
- {
- using var fixture = CreateFixture()
- .AddScoped()
- .AddMudMarkdownServices()
- .BuildServiceProvider();
-
- var instance = fixture.GetRequiredService();
-
- using var scope = fixture.CreateScope();
- var scopedInstance = scope.ServiceProvider.GetRequiredService();
-
- instance
- .Should()
- .NotBe(scopedInstance);
- }
-
[Fact]
public void RegisterServices()
{