feat(ICacheManager): add TryGetCacheEntry method (#5216)

* doc: 增加到期时间列

* test: 增加 benmark 测试功能

* feat: 增加 TryGetCacheEntry 方法

* doc: 增加过期时间

* test: 更新单元测试

* test: 更新单元测试
This commit is contained in:
Argo Zhang
2025-01-26 11:49:08 +08:00
committed by GitHub
parent 01b3e1d17a
commit 895170dc36
15 changed files with 291 additions and 1 deletions

View File

@@ -74,6 +74,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cert", "cert", "{C075C6C8-B
scripts\linux\cert\www.blazor.zone.key = scripts\linux\cert\www.blazor.zone.key
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{9BAF50BE-141D-4429-93A9-942F373D1F68}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTest.Benchmarks", "tools\Benchmarks\UnitTest.Benchmarks.csproj", "{3E6D8D0E-5A36-4CFD-8612-7D64E3FFE7B1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -104,6 +108,10 @@ Global
{D8AEAFE7-10AF-4A5B-BC67-FE740A2CA1DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D8AEAFE7-10AF-4A5B-BC67-FE740A2CA1DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D8AEAFE7-10AF-4A5B-BC67-FE740A2CA1DF}.Release|Any CPU.Build.0 = Release|Any CPU
{3E6D8D0E-5A36-4CFD-8612-7D64E3FFE7B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3E6D8D0E-5A36-4CFD-8612-7D64E3FFE7B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3E6D8D0E-5A36-4CFD-8612-7D64E3FFE7B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3E6D8D0E-5A36-4CFD-8612-7D64E3FFE7B1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -119,6 +127,7 @@ Global
{6D73FED6-0086-460B-84FA-1FA78176BF59} = {7C1D79F1-87BC-42C1-BD5A-CDE4044AC1BD}
{D8AEAFE7-10AF-4A5B-BC67-FE740A2CA1DF} = {7C1D79F1-87BC-42C1-BD5A-CDE4044AC1BD}
{C075C6C8-B9CB-4AC0-9BDF-B2002B4AB99C} = {EA765165-0542-41C8-93F2-85787FEDEDFF}
{3E6D8D0E-5A36-4CFD-8612-7D64E3FFE7B1} = {9BAF50BE-141D-4429-93A9-942F373D1F68}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0DCB0756-34FA-4FD0-AE1D-D3F08B5B3A6B}

View File

@@ -0,0 +1 @@
@ExpirationTime

View File

@@ -0,0 +1,62 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
using Microsoft.Extensions.Caching.Memory;
namespace BootstrapBlazor.Server.Components.Pages;
/// <summary>
/// CacaheExpiration 组件
/// </summary>
public partial class CacaheExpiration
{
[Inject, NotNull]
private ICacheManager? CacheManager { get; set; }
/// <summary>
/// 获得/设置 <see cref="TableColumnContext{TItem, TValue}"/> 实例
/// </summary>
[Parameter, NotNull]
public object? Key { get; set; }
private string? ExpirationTime { get; set; }
/// <summary>
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
await GetCacheEntryExpiration();
}
private async Task GetCacheEntryExpiration()
{
ExpirationTime = "loading ...";
await Task.Yield();
if (CacheManager.TryGetCacheEntry(Key, out ICacheEntry? entry))
{
if (entry.Priority == CacheItemPriority.NeverRemove)
{
ExpirationTime = "Never Remove";
}
else if (entry.SlidingExpiration.HasValue)
{
ExpirationTime = $"Sliding: {entry.SlidingExpiration.Value}";
}
else if (entry.AbsoluteExpiration.HasValue)
{
ExpirationTime = $"Absolute: {entry.AbsoluteExpiration.Value}";
}
else if (entry.ExpirationTokens.Count != 0)
{
ExpirationTime = $"Token: {entry.ExpirationTokens.Count}";
}
}
}
}

View File

@@ -18,6 +18,11 @@
@GetValue(v.Row)
</Template>
</TableTemplateColumn>
<TableTemplateColumn Text="@Localizer["CacheListExpiration"]" Width="160">
<Template Context="v">
<CacaheExpiration Key="v.Row"></CacaheExpiration>
</Template>
</TableTemplateColumn>
<TableTemplateColumn Text="@Localizer["CacheListAction"]" Width="80">
<Template Context="v">
<Button Size="Size.ExtraSmall" Color="Color.Danger" OnClick="() => OnDelete(v.Row)" Icon="fa-solid fa-xmark" Text="@Localizer["CacheListDelete"]"></Button>

View File

@@ -49,7 +49,7 @@ public partial class CacheList
private void UpdateCacheList()
{
_cacheList = CacheManager.Keys.OrderBy(i => i.ToString()).ToList();
_cacheList = [.. CacheManager.Keys.OrderBy(i => i.ToString())];
}
private string GetValue(object key)

View File

@@ -6910,6 +6910,7 @@
"CacheListIntro": "Manage the component library internal cache through the <code>ICacheManager</code> interface method",
"CacheListKey": "Key",
"CacheListValue": "Value",
"CacheListExpiration": "Expiration",
"CacheListAction": "Action",
"CacheListRefresh": "Refresh",
"CacheListDelete": "Delete",

View File

@@ -6910,6 +6910,7 @@
"CacheListIntro": "通过 <code>ICacheManager</code> 接口方法管理组件库内部缓存",
"CacheListKey": "键",
"CacheListValue": "值",
"CacheListExpiration": "到期时间",
"CacheListAction": "操作",
"CacheListRefresh": "刷新",
"CacheListDelete": "删除",

View File

@@ -12,6 +12,7 @@ using System.Linq.Expressions;
using System.Reflection;
#if NET8_0_OR_GREATER
using System.Runtime.CompilerServices;
using System.Collections.Frozen;
#endif
@@ -172,6 +173,53 @@ internal class CacheManager : ICacheManager
return keys;
}
}
private object? _coherentStateInstance = null;
private MethodInfo? _allValuesMethodInfo = null;
/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="key"></param>
/// <param name="entry"></param>
/// <returns></returns>
public bool TryGetCacheEntry(object? key, [NotNullWhen(true)] out ICacheEntry? entry)
{
entry = null;
if (key == null)
{
return false;
}
if (Cache is MemoryCache cache)
{
var values = GetAllValues(cache);
entry = values.Find(e => e.Key == key);
}
return entry != null;
}
private static object GetCoherentState(MemoryCache cache)
{
var fieldInfo = cache.GetType().GetField("_coherentState", BindingFlags.Instance | BindingFlags.NonPublic)!;
return fieldInfo.GetValue(cache)!;
}
private static MethodInfo GetAllValuesMethodInfo(object coherentStateInstance) => coherentStateInstance.GetType().GetMethod("GetAllValues", BindingFlags.Instance | BindingFlags.Public)!;
private List<ICacheEntry> GetAllValues(MemoryCache cache)
{
_coherentStateInstance ??= GetCoherentState(cache);
_allValuesMethodInfo ??= GetAllValuesMethodInfo(_coherentStateInstance);
var ret = new List<ICacheEntry>();
if (_allValuesMethodInfo.Invoke(_coherentStateInstance, null) is IEnumerable<ICacheEntry> values)
{
ret.AddRange(values);
}
return ret;
}
#endif
#region Count

View File

@@ -66,5 +66,13 @@ public interface ICacheManager
/// 获得 缓存键集合
/// </summary>
IEnumerable<object> Keys { get; }
/// <summary>
/// 通过指定 key 获取缓存项 <see cref="ICacheEntry"/> 实例
/// </summary>
/// <param name="key"></param>
/// <param name="entry"></param>
/// <returns></returns>
bool TryGetCacheEntry(object? key, [NotNullWhen(true)] out ICacheEntry? entry);
#endif
}

View File

@@ -0,0 +1,41 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
using System.Runtime.CompilerServices;
namespace UnitTest.Performance;
public class UnsafeAccessorTest
{
[Fact]
public void GetField_Ok()
{
var dummy = new Dummy();
dummy.SetName("test");
Assert.Equal("test", GetNameField(dummy));
}
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_name")]
static extern ref string GetNameField(Dummy @this);
private class Dummy
{
private string? _name;
/// <summary>
///
/// </summary>
/// <returns></returns>
public string? GetName()
{
return _name;
}
public void SetName(string? name)
{
_name = name;
}
}
}

View File

@@ -146,4 +146,18 @@ public class CacheManagerTest : BootstrapBlazorTestBase
return val;
});
}
[Fact]
public void TryGetCacheEntry()
{
Cache.GetOrCreate("test_01", entry =>
{
return 1;
});
Assert.True(Cache.TryGetCacheEntry("test_01", out var entry));
Assert.NotNull(entry);
Assert.False(Cache.TryGetCacheEntry(null, out var v));
Assert.Null(v);
}
}

View File

@@ -0,0 +1,62 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using Microsoft.Diagnostics.Runtime.Utilities;
using System.Dynamic;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace UnitTest.Benchmarks;
[SimpleJob(RuntimeMoniker.Net90)]
public class Benchmarks
{
static readonly Foo _instance = new();
static readonly FieldInfo _privateField = typeof(Foo).GetField("_name", BindingFlags.Instance | BindingFlags.NonPublic)!;
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_name")]
static extern ref string GetNameValue(Foo @this);
[Benchmark]
public object? Reflection()
{
var fieldInfo = typeof(Foo).GetField("_name", BindingFlags.Instance | BindingFlags.NonPublic)!;
return fieldInfo.GetValue(_instance);
}
[Benchmark]
public object? ReflectionWithCache() => _privateField.GetValue(_instance);
[Benchmark]
public string UnsafeAccessor() => GetNameValue(_instance);
[Benchmark]
public string DirectAccess() => _instance.GetName();
[Benchmark]
public string Lambda() => GetFieldValue();
[Benchmark]
public string LambdaWithCache() => GetFooFieldFunc(_instance);
public string GetFieldValue()
{
var method = GetFooFieldExpression().Compile();
return method.Invoke(_instance);
}
private static Func<Foo, string> GetFooFieldFunc = GetFooFieldExpression().Compile();
static Expression<Func<Foo, string>> GetFooFieldExpression()
{
var param_p1 = Expression.Parameter(typeof(Foo));
var body = Expression.Field(param_p1, _privateField);
return Expression.Lambda<Func<Foo, string>>(Expression.Convert(body, typeof(string)), param_p1);
}
}

13
tools/Benchmarks/Foo.cs Normal file
View File

@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
namespace UnitTest.Benchmarks;
public class Foo
{
private string _name = "test";
public string GetName() => _name;
}

View File

@@ -0,0 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
using BenchmarkDotNet.Running;
using UnitTest.Benchmarks;
BenchmarkRunner.Run<Benchmarks>();
Console.ReadKey();

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
</ItemGroup>
</Project>