ABP

2023-06-27 15:01:06

1. 快取模組原始碼解析

個人覺得 ABP 分散式快取模組有三個值得關注的核心點。首先是 AbpRedisCache 類繼承了微軟原生的 RedisCache,並 通過反射的方式獲取RedisCache的私有方法對 RedisCache 進行擴充套件,實現了 ABP 分散式快取中的批次操作方法。

為什麼要這麼做呢?因為基於 Redis 快取的批次操作需要使用到 StackExchange.Redis 原生的SDK,而且在進行操作前總需要先進行 Redis 連線,而相應的方法和屬性原生的 RedisCache 中都有,但是都是私有(private)的,在繼承類中也無法使用,所以使用反射的方式提取出相應的方法和屬性,以便在繼承類中複用。這也是對於類功能進行繼承擴充套件的時候的一種很有用的方式。

第二點是 ABP 快取模組通過 IDistributedCache<CacheItem, CacheKey> 介面擴充套件了原生的 IDistributedCache 介面的功能,而在具體實現上是將原有的 IDistributedCache 服務注入進行復用相應功能的,並在前後增加額外的邏輯對功能進行擴充套件增強,實際上就是介面卡模式。

而最常使用的 IDistributedCache<CacheItem> 介面,以前其實現類是直接繼承 DistributedCache<CacheItem, CacheKey> 的,現在改成了直接用介面卡模式。

最後一個是快取的事務性,通過工作單元使快取和其他事務操作保持原子性,避免快取已經更新而其他事務失敗回滾導致資料不一致的問題。實際上就是先不真正地更新快取,而是將快取資料通過字典儲存在工作單元中,保證一個工作單元內拿到的快取資料是最新的,同時註冊工作單元提交事件,在工作單元正在提交成功的時候才執行真正更新快取的邏輯。

工作單元相關的內容就後面再在專門的章節講吧,這部分的內容比較複雜,一時半會比較難講清。

2. 自己擴充套件的 IDistributedCache

ABP 框架擴充套件的 IDistributedCache 泛型介面在內部幫我們處理範例物件進行序列化/反序列化以及轉碼為 byte 陣列的問題,大大提升了我們使用分散式快取的方便性,但也存在一些問題。

  • 它是基於類的泛型,如果我們在一個服務中需要使用多個快取型別的話,我們就得注入多個的泛型介面,還是有些不方便的
  • 它的快取鍵是以預設是以泛型型別的全類名作為字首,雖然我們可以通過特性指定型別名稱,但是對於集合,快取鍵就很不清晰了
    由於這些情況的存在,也基於我們在日常開發中的使用習慣,我在工作中又基於 ABP 的 IDistributedCache<CacheItem> 介面進行擴充套件。內部實現基本一致,主要就是將基於類的泛型,改成基於方法的泛型,並且提供自己的 IDistributedCacheKeyNormalizer 實現類,將快取鍵的設定規則交給了快取存取時進行設定。 程式碼如下:
public interface IWantDistributedCache
{
	/// <summary>
	/// Gets a cache item with the given key. If no cache item is found for the given key then returns null.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <returns>The cache item, or null.</returns>
	TCacheItem Get<TCacheItem, TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false
	) where TCacheItem : class;

	/// <summary>
	/// Gets multiple cache items with the given keys.
	///
	/// The returned list contains exactly the same count of items specified in the given keys.
	/// An item in the return list can not be null, but an item in the list has null value
	/// if the related key not found in the cache.
	/// </summary>
	/// <param name="keys">The keys of cached items to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <returns>List of cache items.</returns>
	KeyValuePair<TCacheKey, TCacheItem>[] GetMany<TCacheItem, TCacheKey>(
		IEnumerable<TCacheKey> keys,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false
	) where TCacheItem : class;

	/// <summary>
	/// Gets multiple cache items with the given keys.
	///
	/// The returned list contains exactly the same count of items specified in the given keys.
	/// An item in the return list can not be null, but an item in the list has null value
	/// if the related key not found in the cache.
	///
	/// </summary>
	/// <param name="keys">The keys of cached items to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// /// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>List of cache items.</returns>
	Task<KeyValuePair<TCacheKey, TCacheItem>[]> GetManyAsync<TCacheItem, TCacheKey>(
		IEnumerable<TCacheKey> keys,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default
	) where TCacheItem : class;

	/// <summary>
	/// Gets a cache item with the given key. If no cache item is found for the given key then returns null.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The cache item, or null.</returns>
	Task<TCacheItem> GetAsync<TCacheItem, TCacheKey>(
		[NotNull] TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default
	) where TCacheItem : class;

	/// <summary>
	/// Gets or Adds a cache item with the given key. If no cache item is found for the given key then adds a cache item
	/// provided by <paramref name="factory" /> delegate and returns the provided cache item.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="factory">The factory delegate is used to provide the cache item when no cache item is found for the given <paramref name="key" />.</param>
	/// <param name="optionsFactory">The cache options for the factory delegate.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <returns>The cache item.</returns>
	TCacheItem GetOrAdd<TCacheItem, TCacheKey>(
		TCacheKey key,
		Func<TCacheItem> factory,
		string cacheName,
		Func<DistributedCacheEntryOptions> optionsFactory = null,
		bool? hideErrors = null,
		bool considerUow = false
	) where TCacheItem : class;

	/// <summary>
	/// Gets or Adds a cache item with the given key. If no cache item is found for the given key then adds a cache item
	/// provided by <paramref name="factory" /> delegate and returns the provided cache item.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="factory">The factory delegate is used to provide the cache item when no cache item is found for the given <paramref name="key" />.</param>
	/// <param name="optionsFactory">The cache options for the factory delegate.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The cache item.</returns>
	Task<TCacheItem> GetOrAddAsync<TCacheItem, TCacheKey>(
		[NotNull] TCacheKey key,
		Func<Task<TCacheItem>> factory,
		string cacheName,
		Func<DistributedCacheEntryOptions> optionsFactory = null,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default
	) where TCacheItem : class;

	/// <summary>
	/// Sets the cache item value for the provided key.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="value">The cache item value to set in the cache.</param>
	/// <param name="options">The cache options for the value.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	void Set<TCacheItem, TCacheKey>(
		TCacheKey key,
		TCacheItem value,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false
	) where TCacheItem : class;

	/// <summary>
	/// Sets the cache item value for the provided key.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="value">The cache item value to set in the cache.</param>
	/// <param name="options">The cache options for the value.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
	Task SetAsync<TCacheItem, TCacheKey>(
		[NotNull] TCacheKey key,
		[NotNull] TCacheItem value,
		string cacheName,
		[CanBeNull] DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default
	) where TCacheItem : class;

	/// <summary>
	/// Sets multiple cache items.
	/// Based on the implementation, this can be more efficient than setting multiple items individually.
	/// </summary>
	/// <param name="items">Items to set on the cache</param>
	/// <param name="options">The cache options for the value.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	void SetMany<TCacheItem, TCacheKey>(
		IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false
	) where TCacheItem : class;

	/// <summary>
	/// Sets multiple cache items.
	/// Based on the implementation, this can be more efficient than setting multiple items individually.
	/// </summary>
	/// <param name="items">Items to set on the cache</param>
	/// <param name="options">The cache options for the value.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
	Task SetManyAsync<TCacheItem, TCacheKey>(
		IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default
	) where TCacheItem : class;

	/// <summary>
	/// Refreshes the cache value of the given key, and resets its sliding expiration timeout.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	void Refresh<TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null
	);

	/// <summary>
	/// Refreshes the cache value of the given key, and resets its sliding expiration timeout.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
	Task RefreshAsync<TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		CancellationToken token = default
	);

	/// <summary>
	/// Removes the cache item for given key from cache.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	void Remove<TCacheItem, TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false
	) where TCacheItem : class;

	/// <summary>
	/// Removes the cache item for given key from cache.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
	Task RemoveAsync<TCacheItem, TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default
	) where TCacheItem : class;
}

public class WantDistributedCache : IWantDistributedCache
{
	public const string UowCacheName = "WantDistributedCache";

	public ILogger<WantDistributedCache> Logger { get; set; }

	protected string CacheName { get; set; }

	protected bool IgnoreMultiTenancy { get; set; }

	protected IDistributedCache Cache { get; }

	protected ICancellationTokenProvider CancellationTokenProvider { get; }

	protected IDistributedCacheSerializer Serializer { get; }

	protected IDistributedCacheKeyNormalizer KeyNormalizer { get; }

	protected IHybridServiceScopeFactory ServiceScopeFactory { get; }

	protected IUnitOfWorkManager UnitOfWorkManager { get; }

	protected SemaphoreSlim SyncSemaphore { get; }

	protected DistributedCacheEntryOptions DefaultCacheOptions;

	private readonly AbpDistributedCacheOptions _distributedCacheOption;

	public WantDistributedCache(
		IOptions<AbpDistributedCacheOptions> distributedCacheOption,
		IDistributedCache cache,
		ICancellationTokenProvider cancellationTokenProvider,
		IDistributedCacheSerializer serializer,
		IDistributedCacheKeyNormalizer keyNormalizer,
		IHybridServiceScopeFactory serviceScopeFactory,
		IUnitOfWorkManager unitOfWorkManager)
	{
		_distributedCacheOption = distributedCacheOption.Value;
		Cache = cache;
		CancellationTokenProvider = cancellationTokenProvider;
		Logger = NullLogger<WantDistributedCache>.Instance;
		Serializer = serializer;
		KeyNormalizer = keyNormalizer;
		ServiceScopeFactory = serviceScopeFactory;
		UnitOfWorkManager = unitOfWorkManager;

		SyncSemaphore = new SemaphoreSlim(1, 1);

		SetDefaultOptions();
	}

	protected virtual string NormalizeKey<TCacheKey>(TCacheKey key, string cacheName)
	{
		return KeyNormalizer.NormalizeKey(
			new DistributedCacheKeyNormalizeArgs(
				key.ToString(),
				cacheName,
				IgnoreMultiTenancy
			)
		);
	}

	protected virtual DistributedCacheEntryOptions GetDefaultCacheEntryOptions()
	{
		foreach (var configure in _distributedCacheOption.CacheConfigurators)
		{
			var options = configure.Invoke(CacheName);
			if (options != null)
			{
				return options;
			}
		}

		return _distributedCacheOption.GlobalCacheEntryOptions;
	}

	protected virtual void SetDefaultOptions()
	{
		//CacheName = CacheNameAttribute.GetCacheName(typeof(TCacheItem));

		////IgnoreMultiTenancy
		//IgnoreMultiTenancy = typeof(TCacheItem).IsDefined(typeof(IgnoreMultiTenancyAttribute), true);

		//Configure default cache entry options
		DefaultCacheOptions = GetDefaultCacheEntryOptions();
	}

	/// <summary>
	/// Gets a cache item with the given key. If no cache item is found for the given key then returns null.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <returns>The cache item, or null.</returns>
	public virtual TCacheItem Get<TCacheItem, TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false) where TCacheItem : class
	{
		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

		if (ShouldConsiderUow(considerUow))
		{
			var value = GetUnitOfWorkCache<TCacheItem, TCacheKey>().GetOrDefault(key)?.GetUnRemovedValueOrNull();
			if (value != null)
			{
				return value;
			}
		}

		byte[] cachedBytes;

		try
		{
			cachedBytes = Cache.Get(NormalizeKey(key, cacheName));
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				HandleException(ex);
				return null;
			}

			throw;
		}

		return ToCacheItem<TCacheItem>(cachedBytes);
	}

	public virtual KeyValuePair<TCacheKey, TCacheItem>[] GetMany<TCacheItem, TCacheKey>(
		IEnumerable<TCacheKey> keys,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false) where TCacheItem : class
	{
		var keyArray = keys.ToArray();

		var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
		if (cacheSupportsMultipleItems == null)
		{
			return GetManyFallback<TCacheItem, TCacheKey>(
				keyArray,
				cacheName,
				hideErrors,
				considerUow
			);
		}

		var notCachedKeys = new List<TCacheKey>();
		var cachedValues = new List<KeyValuePair<TCacheKey, TCacheItem>>();
		if (ShouldConsiderUow(considerUow))
		{
			var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
			foreach (var key in keyArray)
			{
				var value = uowCache.GetOrDefault(key)?.GetUnRemovedValueOrNull();
				if (value != null)
				{
					cachedValues.Add(new KeyValuePair<TCacheKey, TCacheItem>(key, value));
				}
			}

			notCachedKeys = keyArray.Except(cachedValues.Select(x => x.Key)).ToList();
			if (!notCachedKeys.Any())
			{
				return cachedValues.ToArray();
			}
		}

		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
		byte[][] cachedBytes;

		var readKeys = notCachedKeys.Any() ? notCachedKeys.ToArray() : keyArray;
		try
		{
			cachedBytes = cacheSupportsMultipleItems.GetMany(readKeys.Select(key => NormalizeKey(key, cacheName)));
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				HandleException(ex);
				return ToCacheItemsWithDefaultValues<TCacheItem, TCacheKey>(keyArray);
			}

			throw;
		}

		return cachedValues.Concat(ToCacheItems<TCacheItem, TCacheKey>(cachedBytes, readKeys)).ToArray();
	}

	protected virtual KeyValuePair<TCacheKey, TCacheItem>[] GetManyFallback<TCacheItem, TCacheKey>(
		TCacheKey[] keys,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false) where TCacheItem : class
	{
		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

		try
		{
			return keys
				.Select(key => new KeyValuePair<TCacheKey, TCacheItem>(
						key,
						Get<TCacheItem, TCacheKey>(key, cacheName, false, considerUow)
					)
				).ToArray();
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				HandleException(ex);
				return ToCacheItemsWithDefaultValues<TCacheItem, TCacheKey>(keys);
			}

			throw;
		}
	}

	public virtual async Task<KeyValuePair<TCacheKey, TCacheItem>[]> GetManyAsync<TCacheItem, TCacheKey>(
		IEnumerable<TCacheKey> keys,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default) where TCacheItem : class
	{
		var keyArray = keys.ToArray();

		var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
		if (cacheSupportsMultipleItems == null)
		{
			return await GetManyFallbackAsync<TCacheItem, TCacheKey>(
				keyArray,
				cacheName,
				hideErrors,
				considerUow,
				token
			);
		}

		var notCachedKeys = new List<TCacheKey>();
		var cachedValues = new List<KeyValuePair<TCacheKey, TCacheItem>>();
		if (ShouldConsiderUow(considerUow))
		{
			var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
			foreach (var key in keyArray)
			{
				var value = uowCache.GetOrDefault(key)?.GetUnRemovedValueOrNull();
				if (value != null)
				{
					cachedValues.Add(new KeyValuePair<TCacheKey, TCacheItem>(key, value));
				}
			}

			notCachedKeys = keyArray.Except(cachedValues.Select(x => x.Key)).ToList();
			if (!notCachedKeys.Any())
			{
				return cachedValues.ToArray();
			}
		}

		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
		byte[][] cachedBytes;

		var readKeys = notCachedKeys.Any() ? notCachedKeys.ToArray() : keyArray;

		try
		{
			cachedBytes = await cacheSupportsMultipleItems.GetManyAsync(
				readKeys.Select(key => NormalizeKey(key, cacheName)),
				CancellationTokenProvider.FallbackToProvider(token)
			);
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				await HandleExceptionAsync(ex);
				return ToCacheItemsWithDefaultValues<TCacheItem, TCacheKey>(keyArray);
			}

			throw;
		}

		return cachedValues.Concat(ToCacheItems<TCacheItem, TCacheKey>(cachedBytes, readKeys)).ToArray();
	}

	protected virtual async Task<KeyValuePair<TCacheKey, TCacheItem>[]> GetManyFallbackAsync<TCacheItem, TCacheKey>(
		TCacheKey[] keys,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default) where TCacheItem : class
	{
		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

		try
		{
			var result = new List<KeyValuePair<TCacheKey, TCacheItem>>();

			foreach (var key in keys)
			{
				result.Add(new KeyValuePair<TCacheKey, TCacheItem>(
					key,
					await GetAsync<TCacheItem, TCacheKey>(key, cacheName, false, considerUow, token: token))
				);
			}

			return result.ToArray();
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				await HandleExceptionAsync(ex);
				return ToCacheItemsWithDefaultValues<TCacheItem, TCacheKey>(keys);
			}

			throw;
		}
	}

	/// <summary>
	/// Gets a cache item with the given key. If no cache item is found for the given key then returns null.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The cache item, or null.</returns>
	public virtual async Task<TCacheItem> GetAsync<TCacheItem, TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default) where TCacheItem : class
	{
		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

		if (ShouldConsiderUow(considerUow))
		{
			var value = GetUnitOfWorkCache<TCacheItem, TCacheKey>().GetOrDefault(key)?.GetUnRemovedValueOrNull();
			if (value != null)
			{
				return value;
			}
		}

		byte[] cachedBytes;

		try
		{
			cachedBytes = await Cache.GetAsync(
				NormalizeKey(key, cacheName),
				CancellationTokenProvider.FallbackToProvider(token)
			);
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				await HandleExceptionAsync(ex);
				return null;
			}

			throw;
		}

		if (cachedBytes == null)
		{
			return null;
		}

		return Serializer.Deserialize<TCacheItem>(cachedBytes);
	}

	/// <summary>
	/// Gets or Adds a cache item with the given key. If no cache item is found for the given key then adds a cache item
	/// provided by <paramref name="factory" /> delegate and returns the provided cache item.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="factory">The factory delegate is used to provide the cache item when no cache item is found for the given <paramref name="key" />.</param>
	/// <param name="optionsFactory">The cache options for the factory delegate.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <returns>The cache item.</returns>
	public virtual TCacheItem GetOrAdd<TCacheItem, TCacheKey>(
		TCacheKey key,
		Func<TCacheItem> factory,
		string cacheName,
		Func<DistributedCacheEntryOptions> optionsFactory = null,
		bool? hideErrors = null,
		bool considerUow = false) where TCacheItem : class
	{
		var value = Get<TCacheItem, TCacheKey>(key, cacheName, hideErrors, considerUow);
		if (value != null)
		{
			return value;
		}

		using (SyncSemaphore.Lock())
		{
			value = Get<TCacheItem, TCacheKey>(key, cacheName, hideErrors, considerUow);
			if (value != null)
			{
				return value;
			}

			value = factory();

			if (ShouldConsiderUow(considerUow))
			{
				var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
				if (uowCache.TryGetValue(key, out var item))
				{
					item.SetValue(value);
				}
				else
				{
					uowCache.Add(key, new UnitOfWorkCacheItem<TCacheItem>(value));
				}
			}

			Set(key, value, cacheName, optionsFactory?.Invoke(), hideErrors, considerUow);
		}

		return value;
	}

	/// <summary>
	/// Gets or Adds a cache item with the given key. If no cache item is found for the given key then adds a cache item
	/// provided by <paramref name="factory" /> delegate and returns the provided cache item.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="factory">The factory delegate is used to provide the cache item when no cache item is found for the given <paramref name="key" />.</param>
	/// <param name="optionsFactory">The cache options for the factory delegate.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The cache item.</returns>
	public virtual async Task<TCacheItem> GetOrAddAsync<TCacheItem, TCacheKey>(
		TCacheKey key,
		Func<Task<TCacheItem>> factory,
		string cacheName,
		Func<DistributedCacheEntryOptions> optionsFactory = null,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default) where TCacheItem : class
	{
		token = CancellationTokenProvider.FallbackToProvider(token);
		var value = await GetAsync<TCacheItem, TCacheKey>(key, cacheName, hideErrors, considerUow, token);
		if (value != null)
		{
			return value;
		}

		using (await SyncSemaphore.LockAsync(token))
		{
			value = await GetAsync<TCacheItem, TCacheKey>(key, cacheName, hideErrors, considerUow, token);
			if (value != null)
			{
				return value;
			}

			value = await factory();

			if (ShouldConsiderUow(considerUow))
			{
				var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
				if (uowCache.TryGetValue(key, out var item))
				{
					item.SetValue(value);
				}
				else
				{
					uowCache.Add(key, new UnitOfWorkCacheItem<TCacheItem>(value));
				}
			}

			await SetAsync(key, value, cacheName, optionsFactory?.Invoke(), hideErrors, considerUow, token);
		}

		return value;
	}

	/// <summary>
	/// Sets the cache item value for the provided key.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="value">The cache item value to set in the cache.</param>
	/// <param name="options">The cache options for the value.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	public virtual void Set<TCacheItem, TCacheKey>(
		TCacheKey key,
		TCacheItem value,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false) where TCacheItem : class
	{
		void SetRealCache()
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				Cache.Set(
					NormalizeKey(key, cacheName),
					Serializer.Serialize(value),
					options ?? DefaultCacheOptions
				);
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					HandleException(ex);
					return;
				}

				throw;
			}
		}

		if (ShouldConsiderUow(considerUow))
		{
			var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
			if (uowCache.TryGetValue(key, out _))
			{
				uowCache[key].SetValue(value);
			}
			else
			{
				uowCache.Add(key, new UnitOfWorkCacheItem<TCacheItem>(value));
			}

			// ReSharper disable once PossibleNullReferenceException
			UnitOfWorkManager.Current.OnCompleted(() =>
			{
				SetRealCache();
				return Task.CompletedTask;
			});
		}
		else
		{
			SetRealCache();
		}
	}
	/// <summary>
	/// Sets the cache item value for the provided key.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="value">The cache item value to set in the cache.</param>
	/// <param name="options">The cache options for the value.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
	public virtual async Task SetAsync<TCacheItem, TCacheKey>(
		TCacheKey key,
		TCacheItem value,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default) where TCacheItem : class
	{
		async Task SetRealCache()
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				await Cache.SetAsync(
					NormalizeKey(key, cacheName),
					Serializer.Serialize(value),
					options ?? DefaultCacheOptions,
					CancellationTokenProvider.FallbackToProvider(token)
				);
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					await HandleExceptionAsync(ex);
					return;
				}

				throw;
			}
		}

		if (ShouldConsiderUow(considerUow))
		{
			var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
			if (uowCache.TryGetValue(key, out _))
			{
				uowCache[key].SetValue(value);
			}
			else
			{
				uowCache.Add(key, new UnitOfWorkCacheItem<TCacheItem>(value));
			}

			// ReSharper disable once PossibleNullReferenceException
			UnitOfWorkManager.Current.OnCompleted(SetRealCache);
		}
		else
		{
			await SetRealCache();
		}
	}

	public void SetMany<TCacheItem, TCacheKey>(
		IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false) where TCacheItem : class
	{
		var itemsArray = items.ToArray();

		var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
		if (cacheSupportsMultipleItems == null)
		{
			SetManyFallback(
				itemsArray,
				cacheName,
				options,
				hideErrors,
				considerUow
			);

			return;
		}

		void SetRealCache()
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				cacheSupportsMultipleItems.SetMany(
					ToRawCacheItems(itemsArray, cacheName),
					options ?? DefaultCacheOptions
				);
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					HandleException(ex);
					return;
				}

				throw;
			}
		}

		if (ShouldConsiderUow(considerUow))
		{
			var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();

			foreach (var pair in itemsArray)
			{
				if (uowCache.TryGetValue(pair.Key, out _))
				{
					uowCache[pair.Key].SetValue(pair.Value);
				}
				else
				{
					uowCache.Add(pair.Key, new UnitOfWorkCacheItem<TCacheItem>(pair.Value));
				}
			}

			// ReSharper disable once PossibleNullReferenceException
			UnitOfWorkManager.Current.OnCompleted(() =>
			{
				SetRealCache();
				return Task.CompletedTask;
			});
		}
		else
		{
			SetRealCache();
		}
	}

	protected virtual void SetManyFallback<TCacheItem, TCacheKey>(
		KeyValuePair<TCacheKey, TCacheItem>[] items,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false) where TCacheItem : class
	{
		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

		try
		{
			foreach (var item in items)
			{
				Set(
					item.Key,
					item.Value,
					cacheName,
					options,
					false,
					considerUow
				);
			}
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				HandleException(ex);
				return;
			}

			throw;
		}
	}

	public virtual async Task SetManyAsync<TCacheItem, TCacheKey>(
		IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default) where TCacheItem : class
	{
		var itemsArray = items.ToArray();

		var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
		if (cacheSupportsMultipleItems == null)
		{
			await SetManyFallbackAsync(
				itemsArray,
				cacheName,
				options,
				hideErrors,
				considerUow,
				token
			);

			return;
		}

		async Task SetRealCache()
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				await cacheSupportsMultipleItems.SetManyAsync(
					ToRawCacheItems(itemsArray, cacheName),
					options ?? DefaultCacheOptions,
					CancellationTokenProvider.FallbackToProvider(token)
				);
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					await HandleExceptionAsync(ex);
					return;
				}

				throw;
			}
		}

		if (ShouldConsiderUow(considerUow))
		{
			var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();

			foreach (var pair in itemsArray)
			{
				if (uowCache.TryGetValue(pair.Key, out _))
				{
					uowCache[pair.Key].SetValue(pair.Value);
				}
				else
				{
					uowCache.Add(pair.Key, new UnitOfWorkCacheItem<TCacheItem>(pair.Value));
				}
			}

			// ReSharper disable once PossibleNullReferenceException
			UnitOfWorkManager.Current.OnCompleted(SetRealCache);
		}
		else
		{
			await SetRealCache();
		}
	}

	protected virtual async Task SetManyFallbackAsync<TCacheItem, TCacheKey>(
		KeyValuePair<TCacheKey, TCacheItem>[] items,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default) where TCacheItem : class
	{
		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

		try
		{
			foreach (var item in items)
			{
				await SetAsync(
					item.Key,
					item.Value,
					cacheName,
					options,
					false,
					considerUow,
					token: token
				);
			}
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				await HandleExceptionAsync(ex);
				return;
			}

			throw;
		}
	}

	/// <summary>
	/// Refreshes the cache value of the given key, and resets its sliding expiration timeout.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	public virtual void Refresh<TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null)
	{
		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

		try
		{
			Cache.Refresh(NormalizeKey(key, cacheName));
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				HandleException(ex);
				return;
			}

			throw;
		}
	}

	/// <summary>
	/// Refreshes the cache value of the given key, and resets its sliding expiration timeout.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
	public virtual async Task RefreshAsync<TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		CancellationToken token = default)
	{
		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

		try
		{
			await Cache.RefreshAsync(NormalizeKey(key, cacheName), CancellationTokenProvider.FallbackToProvider(token));
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				await HandleExceptionAsync(ex);
				return;
			}

			throw;
		}
	}

	/// <summary>
	/// Removes the cache item for given key from cache.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	public virtual void Remove<TCacheItem, TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false) where TCacheItem : class
	{
		void RemoveRealCache()
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				Cache.Remove(NormalizeKey(key, cacheName));
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					HandleException(ex);
					return;
				}

				throw;
			}
		}

		if (ShouldConsiderUow(considerUow))
		{
			var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
			if (uowCache.TryGetValue(key, out _))
			{
				uowCache[key].RemoveValue();
			}

			// ReSharper disable once PossibleNullReferenceException
			UnitOfWorkManager.Current.OnCompleted(() =>
			{
				RemoveRealCache();
				return Task.CompletedTask;
			});
		}
		else
		{
			RemoveRealCache();
		}
	}

	/// <summary>
	/// Removes the cache item for given key from cache.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
	public virtual async Task RemoveAsync<TCacheItem, TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default) where TCacheItem : class
	{
		async Task RemoveRealCache()
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				await Cache.RemoveAsync(NormalizeKey(key, cacheName), CancellationTokenProvider.FallbackToProvider(token));
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					await HandleExceptionAsync(ex);
					return;
				}

				throw;
			}
		}

		if (ShouldConsiderUow(considerUow))
		{
			var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
			if (uowCache.TryGetValue(key, out _))
			{
				uowCache[key].RemoveValue();
			}

			// ReSharper disable once PossibleNullReferenceException
			UnitOfWorkManager.Current.OnCompleted(RemoveRealCache);
		}
		else
		{
			await RemoveRealCache();
		}
	}

	protected virtual void HandleException(Exception ex)
	{
		AsyncHelper.RunSync(() => HandleExceptionAsync(ex));
	}

	protected virtual async Task HandleExceptionAsync(Exception ex)
	{
		Logger.LogException(ex, LogLevel.Warning);

		using (var scope = ServiceScopeFactory.CreateScope())
		{
			await scope.ServiceProvider
				.GetRequiredService<IExceptionNotifier>()
				.NotifyAsync(new ExceptionNotificationContext(ex, LogLevel.Warning));
		}
	}

	protected virtual KeyValuePair<TCacheKey, TCacheItem>[] ToCacheItems<TCacheItem, TCacheKey>(byte[][] itemBytes, TCacheKey[] itemKeys) where TCacheItem: class
	{
		if (itemBytes.Length != itemKeys.Length)
		{
			throw new AbpException("count of the item bytes should be same with the count of the given keys");
		}

		var result = new List<KeyValuePair<TCacheKey, TCacheItem>>();

		for (int i = 0; i < itemKeys.Length; i++)
		{
			result.Add(
				new KeyValuePair<TCacheKey, TCacheItem>(
					itemKeys[i],
					ToCacheItem<TCacheItem>(itemBytes[i])
				)
			);
		}

		return result.ToArray();
	}

	[CanBeNull]
	protected virtual TCacheItem ToCacheItem<TCacheItem>([CanBeNull] byte[] bytes) where TCacheItem : class
	{
		if (bytes == null)
		{
			return null;
		}

		return Serializer.Deserialize<TCacheItem>(bytes);
	}


	protected virtual KeyValuePair<string, byte[]>[] ToRawCacheItems<TCacheItem, TCacheKey>(KeyValuePair<TCacheKey, TCacheItem>[] items, string cacheName)
	{
		return items
			.Select(i => new KeyValuePair<string, byte[]>(
					NormalizeKey(i.Key, cacheName),
					Serializer.Serialize(i.Value)
				)
			).ToArray();
	}

	private static KeyValuePair<TCacheKey, TCacheItem>[] ToCacheItemsWithDefaultValues<TCacheItem, TCacheKey>(TCacheKey[] keys)
	{
		return keys
			.Select(key => new KeyValuePair<TCacheKey, TCacheItem>(key, default))
			.ToArray();
	}

	protected virtual bool ShouldConsiderUow(bool considerUow)
	{
		return considerUow && UnitOfWorkManager.Current != null;
	}

	protected virtual string GetUnitOfWorkCacheKey()
	{
		return UowCacheName + CacheName;
	}

	protected virtual Dictionary<TCacheKey, UnitOfWorkCacheItem<TCacheItem>> GetUnitOfWorkCache<TCacheItem, TCacheKey>() where TCacheItem : class
	{
		if (UnitOfWorkManager.Current == null)
		{
			throw new AbpException($"There is no active UOW.");
		}

		return UnitOfWorkManager.Current.GetOrAddItem(GetUnitOfWorkCacheKey(),
			key => new Dictionary<TCacheKey, UnitOfWorkCacheItem<TCacheItem>>());
	}
}

[Dependency(ReplaceServices = true)]
public class WantDistributedCacheKeyNormalizer : IDistributedCacheKeyNormalizer, ITransientDependency
{
	protected AbpDistributedCacheOptions DistributedCacheOptions { get; }

	public SuncereDistributedCacheKeyNormalizer(
		IOptions<AbpDistributedCacheOptions> distributedCacheOptions)
	{
		DistributedCacheOptions = distributedCacheOptions.Value;
	}

	public virtual string NormalizeKey(DistributedCacheKeyNormalizeArgs args)
	{
		// 快取格式: a:appname,c:cachename,k:key
		var normalizedKey = $"a:{DistributedCacheOptions.KeyPrefix},c:{args.CacheName},k:{args.Key}";
		return normalizedKey;
	}
}

[DependsOn(typeof(AbpCachingModule))]
public class WantAbpCacheModule : AbpModule
{
	public override void ConfigureServices(ServiceConfigurationContext context)
	{
		//注入快取類
		context.Services.AddSingleton(typeof(ISuncereDistributedCache), typeof(SuncereDistributedCache));
	}
}


參考文章:
ABP 官方檔案 - 快取



ABP 系列總結:
目錄:ABP 系列總結
上一篇:ABP - 快取模組(1)