From 0d04f6f34b53e21c9b97424d9848c957ca878ef7 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sun, 26 Jan 2025 19:15:57 +0800 Subject: [PATCH] feat(ICacheEntry): add GetLastAccessed extension method (#5219) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * doc: 更改示例代码 * doc: 增加缓存时长描述 * refactor: 更改 OnGetDisplayText 增加可为空 * refactor: 重构 CacheManager 优化性能 * doc: 更新示例 * refactor: 增加扩展方法 * doc: 更新超时时长列内容 * doc: 实现过期时间实时更新 * refactor: 精简代码 * test: 更新单元测试 * test: 更新单元测试 --- .../Pages/CacaheExpiration.razor.cs | 27 +-------- .../Components/Pages/CacheList.razor | 6 +- .../Components/Pages/CacheList.razor.cs | 43 ++++++------- .../Components/Samples/AutoFills.razor.cs | 2 +- .../Components/Samples/InputGroups.razor | 2 +- .../Components/Samples/InputGroups.razor.cs | 6 +- .../Extensions/ICacheEntryExtensions.cs | 52 ++++++++++++++++ .../Components/AutoFill/AutoFill.razor.cs | 2 +- .../Extensions/ICacheEntryExtensions.cs | 43 +++++++++++++ src/BootstrapBlazor/Services/CacheManager.cs | 16 ++--- .../Extensions/ICacheEntryExtensionsTest.cs | 60 +++++++++++++++++++ 11 files changed, 195 insertions(+), 64 deletions(-) create mode 100644 src/BootstrapBlazor.Server/Extensions/ICacheEntryExtensions.cs create mode 100644 src/BootstrapBlazor/Extensions/ICacheEntryExtensions.cs create mode 100644 test/UnitTest/Extensions/ICacheEntryExtensionsTest.cs diff --git a/src/BootstrapBlazor.Server/Components/Pages/CacaheExpiration.razor.cs b/src/BootstrapBlazor.Server/Components/Pages/CacaheExpiration.razor.cs index 19bf5e463e4..9df8217e3fd 100644 --- a/src/BootstrapBlazor.Server/Components/Pages/CacaheExpiration.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Pages/CacaheExpiration.razor.cs @@ -12,14 +12,11 @@ namespace BootstrapBlazor.Server.Components.Pages; /// public partial class CacaheExpiration { - [Inject, NotNull] - private ICacheManager? CacheManager { get; set; } - /// - /// 获得/设置 实例 + /// 获得/设置 实例 /// [Parameter, NotNull] - public object? Key { get; set; } + public object? Context { get; set; } private string? ExpirationTime { get; set; } @@ -39,24 +36,6 @@ 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}"; - } - } + ExpirationTime = Context is ICacheEntry entry ? entry.GetExpiration() : "-"; } } diff --git a/src/BootstrapBlazor.Server/Components/Pages/CacheList.razor b/src/BootstrapBlazor.Server/Components/Pages/CacheList.razor index dc3be3babfb..dc0ab767f60 100644 --- a/src/BootstrapBlazor.Server/Components/Pages/CacheList.razor +++ b/src/BootstrapBlazor.Server/Components/Pages/CacheList.razor @@ -10,7 +10,7 @@ @@ -18,9 +18,9 @@ @GetValue(v.Row) - + diff --git a/src/BootstrapBlazor.Server/Components/Pages/CacheList.razor.cs b/src/BootstrapBlazor.Server/Components/Pages/CacheList.razor.cs index 317ca3ff084..fa083c2516f 100644 --- a/src/BootstrapBlazor.Server/Components/Pages/CacheList.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Pages/CacheList.razor.cs @@ -24,9 +24,11 @@ public partial class CacheList /// /// /// - protected override void OnInitialized() + protected override async Task OnParametersSetAsync() { - base.OnInitialized(); + await base.OnParametersSetAsync(); + + await Task.Yield(); UpdateCacheList(); } @@ -49,30 +51,29 @@ private void OnRefresh() private void UpdateCacheList() { - _cacheList = [.. CacheManager.Keys.OrderBy(i => i.ToString())]; - } - - private string GetValue(object key) - { - string ret = "-"; - if (CacheManager.TryGetValue(key, out object? value)) + _cacheList = CacheManager.Keys.OrderBy(i => i.ToString()).Select(key => { - if (value is string stringValue) + ICacheEntry? entry = null; + if (CacheManager.TryGetCacheEntry(key, out var val)) { - ret = stringValue; - return ret; + entry = val; } + return (object)entry!; + }).ToList(); + } - if (value is IEnumerable) - { - ret = $"{LambdaExtensions.ElementCount(value)}"; - } - else - { - ret = value?.ToString() ?? "-"; - } + private static string GetKey(object data) => data is ICacheEntry entry ? entry.Key.ToString()! : "-"; + + private static string GetValue(object data) => data is ICacheEntry entry ? GetCacheEntryValue(entry) : "-"; + + private static string GetCacheEntryValue(ICacheEntry entry) + { + var value = entry.Value; + if (value is string stringValue) + { + return stringValue; } - return ret; + return value is IEnumerable ? $"{LambdaExtensions.ElementCount(value)}" : value?.ToString() ?? "-"; } } diff --git a/src/BootstrapBlazor.Server/Components/Samples/AutoFills.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/AutoFills.razor.cs index 872874a68da..2803d12ea49 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/AutoFills.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/AutoFills.razor.cs @@ -18,7 +18,7 @@ partial class AutoFills [NotNull] private Foo Model3 { get; set; } = new(); - private static string OnGetDisplayText(Foo foo) => foo.Name ?? ""; + private static string? OnGetDisplayText(Foo? foo) => foo?.Name; [NotNull] private IEnumerable? Items1 { get; set; } diff --git a/src/BootstrapBlazor.Server/Components/Samples/InputGroups.razor b/src/BootstrapBlazor.Server/Components/Samples/InputGroups.razor index 8a7ab7e23c6..31a07fa37d6 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/InputGroups.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/InputGroups.razor @@ -88,7 +88,7 @@
- +
diff --git a/src/BootstrapBlazor.Server/Components/Samples/InputGroups.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/InputGroups.razor.cs index acee7641ea3..6f615df2348 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/InputGroups.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/InputGroups.razor.cs @@ -54,9 +54,9 @@ protected override void OnInitialized() private readonly IEnumerable Items = new SelectedItem[] { - new SelectedItem("", "请选择 ..."), - new SelectedItem("Beijing", "北京"), - new SelectedItem("Shanghai", "上海") + new("", "请选择 ..."), + new("Beijing", "北京"), + new("Shanghai", "上海") }; private string? GroupFormClassString => CssBuilder.Default("row g-3").AddClass("form-inline", FormRowType == RowType.Inline).Build(); diff --git a/src/BootstrapBlazor.Server/Extensions/ICacheEntryExtensions.cs b/src/BootstrapBlazor.Server/Extensions/ICacheEntryExtensions.cs new file mode 100644 index 00000000000..dcfa675daa6 --- /dev/null +++ b/src/BootstrapBlazor.Server/Extensions/ICacheEntryExtensions.cs @@ -0,0 +1,52 @@ +// 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; +using System.Reflection; + +namespace BootstrapBlazor.Server.Extensions; + +/// +/// 扩展方法 +/// +public static class ICacheEntryExtensions +{ + /// + /// 获取缓存项过期时间 + /// + /// + /// + public static string GetExpiration(this ICacheEntry entry) + { + string? ret; + if (entry.Priority == CacheItemPriority.NeverRemove) + { + ret = "Never Remove"; + } + else if (entry.SlidingExpiration.HasValue) + { + ret = $"Sliding: {entry.GetSlidingLeftTime().TotalSeconds:###}/{entry.SlidingExpiration.Value.TotalSeconds}"; + } + else if (entry.AbsoluteExpiration.HasValue) + { + ret = $"Absolute: {entry.AbsoluteExpiration.Value}"; + } + else if (entry.ExpirationTokens.Count != 0) + { + ret = $"Token: {entry.ExpirationTokens.Count}"; + } + else + { + ret = "Not Set"; + } + return ret; + } + + private static TimeSpan GetSlidingLeftTime(this ICacheEntry entry) + { + var lastAccessed = entry.GetLastAccessed(); + return lastAccessed == null ? TimeSpan.Zero : entry.SlidingExpiration!.Value - (DateTime.UtcNow - lastAccessed.Value); + } +} diff --git a/src/BootstrapBlazor/Components/AutoFill/AutoFill.razor.cs b/src/BootstrapBlazor/Components/AutoFill/AutoFill.razor.cs index 740562d17f5..11eff0e19e1 100644 --- a/src/BootstrapBlazor/Components/AutoFill/AutoFill.razor.cs +++ b/src/BootstrapBlazor/Components/AutoFill/AutoFill.razor.cs @@ -58,7 +58,7 @@ public partial class AutoFill /// [Parameter] [NotNull] - public Func? OnGetDisplayText { get; set; } + public Func? OnGetDisplayText { get; set; } /// /// 图标 diff --git a/src/BootstrapBlazor/Extensions/ICacheEntryExtensions.cs b/src/BootstrapBlazor/Extensions/ICacheEntryExtensions.cs new file mode 100644 index 00000000000..925fefcf6fa --- /dev/null +++ b/src/BootstrapBlazor/Extensions/ICacheEntryExtensions.cs @@ -0,0 +1,43 @@ +// 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; +using System.Reflection; + +namespace BootstrapBlazor.Components; + +/// +/// 扩展方法 +/// +public static class ICacheEntryExtensions +{ + /// + /// 获得缓存项 最后访问时间 + /// + /// + /// + /// + public static DateTime? GetLastAccessed(this ICacheEntry entry, bool force = false) + { + if (force) + { + _lastAccessedProperty = null; + } + _lastAccessedProperty ??= entry.GetType().GetProperty("LastAccessed", BindingFlags.Instance | BindingFlags.NonPublic); + + DateTime? ret = null; + if (_lastAccessedProperty != null) + { + var v = _lastAccessedProperty.GetValue(entry); + if (v is DateTime val) + { + ret = val; + } + } + return ret; + } + + private static PropertyInfo? _lastAccessedProperty = null; +} diff --git a/src/BootstrapBlazor/Services/CacheManager.cs b/src/BootstrapBlazor/Services/CacheManager.cs index 4b7ba4ce2cc..bed6403e9c3 100644 --- a/src/BootstrapBlazor/Services/CacheManager.cs +++ b/src/BootstrapBlazor/Services/CacheManager.cs @@ -178,6 +178,10 @@ public IEnumerable Keys private MethodInfo? _allValuesMethodInfo = null; + private static readonly FieldInfo _coherentStateFieldInfo = typeof(MemoryCache).GetField("_coherentState", BindingFlags.Instance | BindingFlags.NonPublic)!; + + private static MethodInfo GetAllValuesMethodInfo(Type type) => type.GetMethod("GetAllValues", BindingFlags.Instance | BindingFlags.Public)!; + /// /// /// @@ -200,18 +204,10 @@ public bool TryGetCacheEntry(object? key, [NotNullWhen(true)] out ICacheEntry? e 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 GetAllValues(MemoryCache cache) { - _coherentStateInstance ??= GetCoherentState(cache); - _allValuesMethodInfo ??= GetAllValuesMethodInfo(_coherentStateInstance); + _coherentStateInstance = _coherentStateFieldInfo.GetValue(cache)!; + _allValuesMethodInfo ??= GetAllValuesMethodInfo(_coherentStateInstance.GetType()); var ret = new List(); if (_allValuesMethodInfo.Invoke(_coherentStateInstance, null) is IEnumerable values) diff --git a/test/UnitTest/Extensions/ICacheEntryExtensionsTest.cs b/test/UnitTest/Extensions/ICacheEntryExtensionsTest.cs new file mode 100644 index 00000000000..3286c3eed43 --- /dev/null +++ b/test/UnitTest/Extensions/ICacheEntryExtensionsTest.cs @@ -0,0 +1,60 @@ +// 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; +using Microsoft.Extensions.Primitives; + +namespace UnitTest.Extensions; + +public class ICacheEntryExtensionsTest : BootstrapBlazorTestBase +{ + [Fact] + public void GetLastAccessed_Ok() + { + Cache.GetOrCreate("test_01", entry => + { + return 1; + }); + + Assert.True(Cache.TryGetCacheEntry("test_01", out var entry)); + var v = entry.GetLastAccessed(true); + Assert.NotNull(v); + } + + [Fact] + public void GetLastAccessed_Null() + { + var mock = new MockCacheEntry(); + var v = mock.GetLastAccessed(true); + Assert.Null(v); + } + + class MockCacheEntry : ICacheEntry + { + public object Key { get; } + public object? Value { get; set; } + public DateTimeOffset? AbsoluteExpiration { get; set; } + public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; } + public TimeSpan? SlidingExpiration { get; set; } + public IList ExpirationTokens { get; } + public IList PostEvictionCallbacks { get; } + public CacheItemPriority Priority { get; set; } + public long? Size { get; set; } + + private int LastAccessed { get; set; } + + public MockCacheEntry() + { + Key = "_test"; + ExpirationTokens = []; + PostEvictionCallbacks = []; + } + + public void Dispose() + { + throw new NotImplementedException(); + } + } +}