Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions samples/Server/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Microsoft.Extensions.Options;
using MudBlazor.Markdown.Core.Utils.ServiceRegistration;
using MudBlazor.Services;

Expand All @@ -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);
});
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace MudBlazor;

public sealed class MudMarkdownMemoryCacheOptions
{
public TimeSpan TimeToLive { get; set; }
}
1 change: 0 additions & 1 deletion src/MudBlazor.Markdown/MudBlazor.Markdown.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

<ItemGroup>
<PackageReference Include="Markdig" Version="0.41.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.5" />
<PackageReference Include="MudBlazor" Version="8.7.0" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
84 changes: 78 additions & 6 deletions src/MudBlazor.Markdown/Services/MudMarkdownMemoryCache.cs
Original file line number Diff line number Diff line change
@@ -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<MemoryCacheOptions> 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<string, Entry> _memoryCache = new();
private readonly long _ttl;

public MudMarkdownMemoryCache(IOptions<MudMarkdownMemoryCacheOptions> 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<MemoryCacheOptions> 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;
}
}
}
12 changes: 5 additions & 7 deletions src/MudBlazor.Markdown/Services/MudMarkdownValueProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MudMarkdownMemoryCacheEntryOptions> options)
public MudMarkdownValueProvider(IMudMarkdownMemoryCache memoryCache)
{
_memoryCache = memoryCache;
_memoryCacheEntryOptions = options.Value;
}

public async ValueTask<string> GetValueAsync(string value, MarkdownSourceType sourceType, CancellationToken ct = default)
Expand All @@ -27,7 +25,7 @@ public async ValueTask<string> GetValueAsync(string value, MarkdownSourceType so

private async ValueTask<string> ReadFromFileAsync(string path, CancellationToken ct = default)
{
if (_memoryCache.TryGetValue<string>(path, out var value) && value is not null)
if (_memoryCache.TryGetValue(path, out var value))
return value;

try
Expand All @@ -38,7 +36,7 @@ private async ValueTask<string> 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)
Expand All @@ -51,15 +49,15 @@ private async ValueTask<string> ReadFromFileAsync(string path, CancellationToken

private async ValueTask<string> ReadFromUrlAsync(string url, CancellationToken ct = default)
{
if (_memoryCache.TryGetValue<string>(url, out var value) && value is not null)
if (_memoryCache.TryGetValue(url, out var value))
return value;

try
{
value = await HttpClient.GetStringAsync(url, ct)
.ConfigureAwait(false);

_memoryCache.Set(url, value, _memoryCacheEntryOptions);
_memoryCache.Set(url, value);
return value;
}
catch (Exception e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,26 @@

public static class ServiceCollectionEx
{
public static IServiceCollection AddMudMarkdownServices(this IServiceCollection @this, Action<MudMarkdownMemoryCacheEntryOptions>? configureMemoryCache = null)
public static IServiceCollection AddMudMarkdownServices(this IServiceCollection @this, Action<MudMarkdownMemoryCacheOptions>? configureMemoryCache = null)
{
return @this
.AddMudMarkdownCache(configureMemoryCache)
.AddScoped<IMudMarkdownThemeService, MudMarkdownThemeService>()
.AddSingleton<IMudMarkdownValueProvider, MudMarkdownValueProvider>();
}

private static IServiceCollection AddMudMarkdownCache(this IServiceCollection @this, Action<MudMarkdownMemoryCacheEntryOptions>? configureMemoryCache)
private static IServiceCollection AddMudMarkdownCache(this IServiceCollection @this, Action<MudMarkdownMemoryCacheOptions>? configureMemoryCache)
{
return @this
.AddOptions()
.AddSingleton<IMudMarkdownMemoryCache, MudMarkdownMemoryCache>()
.Configure<MudMarkdownMemoryCacheEntryOptions>(options =>
.Configure<MudMarkdownMemoryCacheOptions>(options =>
{
if (configureMemoryCache != null)
{
if (configureMemoryCache is not null)
configureMemoryCache(options);
}
else
{
options.SlidingExpiration = TimeSpan.FromHours(1);
}
});
options.TimeToLive = TimeSpan.FromHours(1);
})
.AddSingleton<IMudMarkdownMemoryCache, MudMarkdownMemoryCache>();
}

public static IServiceCollection AddMudMarkdownClipboardService<T>(this IServiceCollection @this)
Expand Down
2 changes: 0 additions & 2 deletions src/MudBlazor.Markdown/_Usings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
72 changes: 72 additions & 0 deletions tests/MudBlazor.Markdown.Tests/Assertions.cs
Original file line number Diff line number Diff line change
@@ -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<IMudMarkdownMemoryCache?, MudMarkdownMemoryCacheAssertions>(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<string>()
.Single()
.Should()
.Be(key);
}

public void HaveKeys(IReadOnlyList<string> 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.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<PackageReference Include="MoqMicrosoftConfiguration" Version="1.0.5" />
<PackageReference Include="MyNihongo.Option" Version="2.2.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.0">
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IMudMarkdownMemoryCache>();
}

public void Dispose()
{
GC.SuppressFinalize(this);
_serviceProvider?.Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading