using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
public class DuplicateFreeCache<TKey, TItem> where TItem : class
private ConcurrentDictionary<TKey, int> Primary { get; } = new ConcurrentDictionary<TKey, int>();
private List<TItem> ItemList { get; } = new List<TItem>();
private List<TItem[]> ListList { get; } = new List<TItem[]>();
private Dictionary<TItem, int> ItemDict { get; } = new Dictionary<TItem, int>();
private Dictionary<IntArray, int> ListDict { get; } = new Dictionary<IntArray, int>();
public IReadOnlyList<TItem> GetOrAdd(TKey key, Func<TKey, IReadOnlyList<TItem>> getFunc)
int index = Primary.GetOrAdd(key, k =>
var rawList = getFunc(k);
int[] itemListByIndex = rawList.Select(item =>
if (!ItemDict.TryGetValue(item, out int itemIndex))
itemIndex = ItemList.Count;
ItemDict[item] = itemIndex;
var intArray = new IntArray(itemListByIndex);
if (!ListDict.TryGetValue(intArray, out int listIndex))
listIndex = ListList.Count;
ListList.Add(itemListByIndex.Select(ii => ItemList[ii]).ToArray());
ListDict[intArray] = listIndex;
public override string ToString()
StringBuilder sb = new StringBuilder();
sb.AppendLine($"A cache with:");
sb.AppendLine($"{ItemList.Count} unique Items;");
sb.AppendLine($"{ListList.Count} unique lists of Items;");
sb.AppendLine($"{Primary.Count} primary dictionary items;");
sb.AppendLine($"{ItemDict.Count} item dictionary items;");
sb.AppendLine($"{ListDict.Count} list dictionary items;");
private readonly int _hashCode;
public int[] Array { get; }
public IntArray(int[] arr)
for (int i = 0; i < arr.Length; i++)
_hashCode = (_hashCode * 397) ^ arr[i];
protected bool Equals(IntArray other)
return Array.SequenceEqual(other.Array);
public override bool Equals(object obj)
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((IntArray)obj);
public override int GetHashCode() => _hashCode;
public string Str { get; }
public TestItem(string s)
protected bool Equals(TestItem other)
return string.Equals(Str, other.Str, StringComparison.OrdinalIgnoreCase);
public override bool Equals(object obj)
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((TestItem)obj);
public override int GetHashCode()
return (Str != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Str) : 0);
public static class Program
private static Random _r = new Random();
static List<TestItem> GetRandomItemList(int maxItems, int maxItemNumber)
int count = _r.Next(1, maxItems);
return Enumerable.Range(0, count).Select(i => new TestItem(_r.Next(maxItemNumber).ToString())).ToList();
public static void Main()
DuplicateFreeCache<string, TestItem> cache = new DuplicateFreeCache<string, TestItem>();
Parallel.For(0, 100000, (i) =>
var rand = new Random(i);
string key = rand.Next(1000).ToString();
var list = GetRandomItemList(15, 5);
cache.GetOrAdd(key, (k) => list);
Console.WriteLine(cache.ToString());