/// <summary> /// Check the cache. /// </summary> /// <param name="key">Key to look up.</param> /// <param name="result">The value result - either from cache or freshly determined.</param> /// <param name="valueFactory">A callback that will provide the values.</param> /// <returns></returns> protected bool TryGetOrAdd(TKey key, out TValue result, Func <TKey, TValue> valueFactory) { bool fromCache = false; // Wrap value-factory to handle cache invalidator Func <TKey, TValue> valueFactoryImpl = (k) => { TValue innerResult; using (CacheContext cacheContext = new CacheContext()) { innerResult = valueFactory(key); // Add the cache context entries to the appropriate CacheInvalidator _cacheInvalidator.AddInvalidations(cacheContext, key); } return(innerResult); }; // Check cache fromCache = Cache.TryGetOrAdd(key, out result, valueFactoryImpl); if (fromCache && CacheContext.IsSet()) { // Add the already stored changes that should invalidate this cache // entry to any outer or containing cache contexts. using (CacheContext cacheContext = CacheContext.GetContext()) { cacheContext.AddInvalidationsFor(_cacheInvalidator, key); } } return(fromCache); }
/// <summary> /// Load the roles recursively for a user or role. /// </summary> /// <param name="subjectId"> /// The user to load the roles for. This cannot be negative. /// </param> /// <returns> /// The roles the user is recursively a member of. /// </returns> /// <exception cref="ArgumentException"> /// <paramref name="subjectId"/> cannot be negative. /// </exception> public ISet <long> GetUserRoles(long subjectId) { ISet <long> roles; if (!Cache.TryGetValue(subjectId, out roles)) { using (CacheContext cacheContext = new CacheContext()) { roles = RoleRepository.GetUserRoles(subjectId); Cache[subjectId] = roles; _cacheInvalidator.AddInvalidations(cacheContext, subjectId); } } else { // Add the already stored changes that should invalidate this cache // entry to any outer or containing cache contexts. using (CacheContext cacheContext = CacheContext.GetContext( )) { cacheContext.AddInvalidationsFor(_cacheInvalidator, subjectId); } } return(roles); }
public void Test_AddInvalidationsFor_NullCacheInvalidator() { using (CacheContext cacheContext = new CacheContext()) { Assert.That(() => cacheContext.AddInvalidationsFor <int, int>(null, 1), Throws.TypeOf <ArgumentNullException>().And.Property("ParamName").EqualTo("cacheInvalidator")); } }
/// <summary> /// Execute a request for bulk data from the SQL database. /// </summary> /// <param name="request">The requested data</param> /// <returns></returns> public BulkRequestResult GetBulkResult(EntityRequest request) { if (request == null) { throw new ArgumentNullException("request"); } // Bypass cache if (request.IgnoreResultCache) { return(CreateResult(request)); } BulkRequestResult result; CachingBulkRequestRunnerValue cacheValue; CachingBulkRequestRunnerKey key = CachingBulkRequestRunnerKey.Create(request); // Check cache bool inCache = Cache.TryGetValue(key, out cacheValue); // Should parent cache contexts be notified of invalidations // .. no for now, for compatibility with previous system. Consider changing bool notifyParentCacheContext = false; if (!inCache) { using (var cacheContext = new CacheContext(notifyParentCacheContext ? ContextType.New : ContextType.Detached)) // Detached for now.. { result = CreateResult(request); cacheValue = new CachingBulkRequestRunnerValue(result); Cache.Add(key, cacheValue); // Add the cache context entries to the appropriate CacheInvalidator cacheContext.Entities.Add(result.AllEntities.Keys); cacheContext.EntityInvalidatingRelationshipTypes.Add(GetRelationshipTypesUsed(result)); _cacheInvalidator.AddInvalidations(cacheContext, key); } } else { if (notifyParentCacheContext && CacheContext.IsSet( )) { using (CacheContext cacheContext = new CacheContext(ContextType.Attached)) { // Add the already stored changes that should invalidate this cache // entry to any outer or containing cache contexts. cacheContext.AddInvalidationsFor(_cacheInvalidator, key); } } } result = cacheValue.BulkRequestResult; request.ResultFromCache = inCache; // TODO: Find a better channel to return this info. (It can't be in the response, because that's cached) return(result); }
/// <summary> /// Convert a <see cref="Report" /> to a <see cref="StructuredQuery" />. /// </summary> /// <param name="report">The <see cref="Report" /> to convert. This cannot be null.</param> /// <param name="settings"></param> /// <returns> /// The converted report. /// </returns> /// <exception cref="System.ArgumentNullException">report</exception> public StructuredQuery Convert(Report report, ReportToQueryConverterSettings settings) { if (report == null) { throw new ArgumentNullException("report"); } if (settings == null) { settings = ReportToQueryConverterSettings.Default; } StructuredQuery result; CachingReportToQueryConverterValue cacheValue; CachingReportToQueryConverterKey key = new CachingReportToQueryConverterKey(report, settings); using (MessageContext msg = new MessageContext("Reports")) { // Check cache bool doConvert = !TryGetValue(key, msg, out cacheValue); // Check for force recalculation if (settings.RefreshCachedStructuredQuery) { msg.Append(() => "CachingReportToQueryConverter refreshed forced"); doConvert = true; } if (doConvert) { lock (_syncRoot) { using (CacheContext cacheContext = new CacheContext( )) { result = Converter.Convert(report, settings); cacheValue = new CachingReportToQueryConverterValue(result); Cache.Add(key, cacheValue); // Add the cache context entries to the appropriate CacheInvalidator _cacheInvalidator.AddInvalidations(cacheContext, key); } } } else if (CacheContext.IsSet( )) { // Add the already stored changes that should invalidate this cache // entry to any outer or containing cache contexts. using (CacheContext cacheContext = CacheContext.GetContext( )) { cacheContext.AddInvalidationsFor(_cacheInvalidator, key); } } } result = cacheValue.StructuredQuery; return(result); }
/// <summary> /// Build an <see cref="EntityMemberRequest"/> used to look for entities /// to perform additional security checks on. /// </summary> /// <param name="entityType"> /// The type of entity whose security is being checked. This cannot be null. /// </param> /// <param name="permissions">The type of permissions required.</param> /// <returns> /// The <see cref="EntityMemberRequest"/>. /// </returns> /// <exception cref="ArgumentNullException"> /// <paramref name="entityType"/> cannot be null. /// </exception> public EntityMemberRequest BuildEntityMemberRequest(EntityType entityType, IList <EntityRef> permissions) { if (entityType == null) { throw new ArgumentNullException("entityType"); } bool isModify = false; if (permissions != null) { foreach (EntityRef perm in permissions) { if (perm.Id == Permissions.Modify.Id || perm.Id == Permissions.Delete.Id) { isModify = true; break; } } } // negative keys for 'modify', +ve keys for read // because #27911 popped up just hours before the branch of a feature that got promised to someone long cacheKey = isModify ? -entityType.Id : entityType.Id; EntityMemberRequest result; if (!Cache.TryGetValue(cacheKey, out result)) { using (CacheContext cacheContext = new CacheContext()) { result = Factory.BuildEntityMemberRequest(entityType, permissions); Cache.Add(cacheKey, result); _cacheInvalidator.AddInvalidations(cacheContext, cacheKey); } } else if (CacheContext.IsSet( )) { // Add the already stored changes that should invalidate this cache // entry to any outer or containing cache contexts. using (CacheContext cacheContext = CacheContext.GetContext( )) { cacheContext.AddInvalidationsFor(_cacheInvalidator, cacheKey); } } return(result); }
/// <summary> /// Gets a value that represents a hash of the IDs of a set of security rules. /// That is, if two users have equatable UserRuleSets then the same security rules apply to both. /// </summary> /// <param name="userId"> /// The ID of the user <see cref="UserAccount"/>. /// This cannot be negative. /// </param> /// <param name="permission"> /// The permission to get the query for. This cannot be null and should be one of <see cref="Permissions.Read"/>, /// <see cref="Permissions.Modify"/> or <see cref="Permissions.Delete"/>. /// </param> public UserRuleSet GetUserRuleSet(long userId, EntityRef permission) { // Validate if (userId <= 0) { throw new ArgumentNullException("userId"); } if (permission == null) { throw new ArgumentNullException("permission"); } if (permission.Id <= 0) { throw new ArgumentException("permission"); } // Create cache key CachingUserRuleSetProviderKey key = new CachingUserRuleSetProviderKey(userId, permission.Id); UserRuleSet result; Func <CachingUserRuleSetProviderKey, UserRuleSet> valueFactory = (k) => { UserRuleSet innerResult; using (CacheContext cacheContext = new CacheContext()) { innerResult = InnerProvider.GetUserRuleSet(k.SubjectId, k.PermissionId); // Add the cache context entries to the appropriate CacheInvalidator _cacheInvalidator.AddInvalidations(cacheContext, key); } return(innerResult); }; // Check cache if (Cache.TryGetOrAdd(key, out result, valueFactory) && CacheContext.IsSet()) { // Add the already stored changes that should invalidate this cache // entry to any outer or containing cache contexts. using (CacheContext cacheContext = CacheContext.GetContext( )) { cacheContext.AddInvalidationsFor(_cacheInvalidator, key); } } return(result); }
/// <summary> /// Get the queries for a given user and permission or operation. /// </summary> /// <param name="subjectId"> /// The ID of the <see cref="Subject"/>, that is a <see cref="UserAccount"/> or <see cref="Role"/> instance. /// This cannot be negative. /// </param> /// <param name="permission"> /// The permission to get the query for. This should be one of <see cref="Permissions.Read"/>, /// <see cref="Permissions.Modify"/> or <see cref="Permissions.Delete"/>. Or null to match all permissions. /// </param> /// <param name="securableEntityTypes"> /// The IDs of <see cref="SecurableEntity"/> types being accessed. Or null to match all entity types. /// </param> /// <returns> /// The queries to run. /// </returns> /// <exception cref="ArgumentException"> /// <paramref name="subjectId"/> does not exist. Also, <paramref name="permission"/> should /// be one of <see cref="Permissions.Read"/>, <see cref="Permissions.Modify"/> or <see cref="Permissions.Delete"/> /// </exception> /// <exception cref="ArgumentNullException"> /// Neither <paramref name="permission"/> nor <paramref name="securableEntityTypes"/> can be null. /// </exception> public IEnumerable <AccessRuleQuery> GetQueries(long subjectId, [CanBeNull] Model.EntityRef permission, [CanBeNull] IList <long> securableEntityTypes) { SubjectPermissionTypesTuple tuple; IEnumerable <AccessRuleQuery> result; result = null; tuple = new SubjectPermissionTypesTuple(subjectId, permission, securableEntityTypes); if (!Cache.TryGetValue(tuple, out result)) { using (CacheContext cacheContext = new CacheContext( )) { result = Repository.GetQueries(subjectId, permission, securableEntityTypes).ToList( ); try { Cache.Add(tuple, result); // Add the cache context entries to the appropriate CacheInvalidator _cacheInvalidator.AddInvalidations(cacheContext, tuple); } catch (ArgumentException) { Trace.WriteLine("CachingQueryRepository: Entity already in cache"); throw; } } } else if (CacheContext.IsSet( )) { using (CacheContext cacheContext = CacheContext.GetContext( )) { // Add the already stored changes that should invalidate this cache // entry to any outer or containing cache contexts. cacheContext.AddInvalidationsFor(_cacheInvalidator, tuple); } } return(result); }
/// <summary> /// Convert a <see cref="Report" /> to a <see cref="StructuredQuery" />. /// </summary> /// <param name="typeIds">The type ids.</param> /// <returns> /// The converted report. /// </returns> /// <exception cref="System.ArgumentNullException">report</exception> public IEnumerable <Workflow> Fetch(ISet <long> typeIds) { if (typeIds == null) { throw new ArgumentNullException("typeIds"); } CachingWorkflowActionsFactoryValue cacheValue; var key = new CachingWorkflowActionsFactoryKey(typeIds); // Check cache bool found = Cache.TryGetOrAdd(key, out cacheValue, (innerKey) => { using (CacheContext cacheContext = new CacheContext( )) { var result = new CachingWorkflowActionsFactoryValue(_fetcher.Fetch(innerKey.TypeIds)); // Add the cache context entries to the appropriate CacheInvalidator _cacheInvalidator.AddInvalidations(cacheContext, key); return(result); } } ); if (found && CacheContext.IsSet( )) { // Add the already stored changes that should invalidate this cache // entry to any outer or containing cache contexts. using (CacheContext cacheContext = CacheContext.GetContext( )) { cacheContext.AddInvalidationsFor(_cacheInvalidator, key); } } return(cacheValue.Workflows); }
/// <summary> /// Re-used cache checking logic that can be used for caching eithe instances lookups or type lookups. /// </summary> /// <param name="entities">The entities/types to check. This cannot be null or contain null.</param> /// <param name="permissions">The permissions or operations to check. This cannot be null or contain null.</param> /// <param name="user">The user requesting access. This cannot be null.</param> /// <param name="entityIdCallback">Callback used to determine how to get the ID of TEntity.</param> /// <param name="innerCheckAccessCallback">Callback used to perform the uncached check.</param> /// <returns>A mapping of each entity ID to whether the user has access (true) or not (false).</returns> private IDictionary <long, bool> CachedCheckImpl <TEntity>(IList <TEntity> entities, IList <EntityRef> permissions, EntityRef user, Func <TEntity, long> entityIdCallback, Func <IList <TEntity>, IList <EntityRef>, EntityRef, IDictionary <long, bool> > innerCheckAccessCallback) { TKey cacheKey; CachingEntityAccessControlCheckerResult result; IList <TEntity> toCheckEntities; long[] permissionIdArray; IDictionary <long, bool> innerResult; // If SecurityBypassContext is active, avoid the cache. Otherwise, // the cache will remember the user had access to entities that // they may not have. if (EntityAccessControlChecker.SkipCheck(user)) { innerResult = innerCheckAccessCallback(entities, permissions, user); return(innerResult); } result = new CachingEntityAccessControlCheckerResult(); toCheckEntities = null; permissionIdArray = permissions.Select(x => x.Id).ToArray(); // Determine uncached entities using (CacheContext cacheContext = CacheContext.IsSet( ) ? CacheContext.GetContext( ) : null) { foreach (TEntity entity in entities) { long entityId = entityIdCallback(entity); cacheKey = CreateKey(user.Id, entityId, permissionIdArray); bool cacheEntry; if (Cache.TryGetValue(cacheKey, out cacheEntry)) { result.CacheResult[entityId] = cacheEntry; // Add the already stored changes that should invalidate this cache // entry to any outer or containing cache contexts. if (cacheContext != null) { cacheContext.AddInvalidationsFor(_cacheInvalidator, cacheKey); } } else { if (toCheckEntities == null) { toCheckEntities = new List <TEntity>(); } toCheckEntities.Add(entity); } } } LogMessage(result); if (toCheckEntities != null) { using (CacheContext cacheContext = new CacheContext( )) { innerResult = innerCheckAccessCallback(toCheckEntities, permissions, user); foreach (KeyValuePair <long, bool> entry in innerResult) { long entityId = entry.Key; bool accessGranted = entry.Value; result.Add(entityId, accessGranted); // Cache the results cacheKey = CreateKey(user.Id, entry.Key, permissionIdArray); Cache [cacheKey] = accessGranted; // Add the cache context entries to the appropriate CacheInvalidator _cacheInvalidator.AddInvalidations(cacheContext, cacheKey); } } } // Add the results from the originally cached entities foreach (KeyValuePair <long, bool> entry in result.CacheResult) { result[entry.Key] = entry.Value; } return(result); }
/// <summary> /// Build the SQL, or collect it from cache. /// </summary> /// <param name="query"></param> /// <param name="settings"></param> /// <returns></returns> public QueryResult ExecuteQuery(StructuredQuery query, QuerySettings settings) { // Validate if (query == null) { throw new ArgumentNullException("query"); } if (QueryRunner == null) { throw new InvalidOperationException("QueryRunner not set."); } if (settings == null) { settings = new QuerySettings( ); } // Determine if we should cache .. and the cache key QueryBuild builtQuery; CachingQueryRunnerKey key; CacheContext queryBuilderCacheContext; using (queryBuilderCacheContext = new CacheContext()) { key = CreateCacheKeyAndQuery(query, settings, out builtQuery); } // A null key means that the ersult should not participate in caching if (key == null) { return(RunQueryImpl(query, settings, builtQuery)); } CachingQueryRunnerValue cacheValue; using (MessageContext msg = new MessageContext("Reports")) { // Check for force recalculation if (settings.RefreshCachedResult) { msg.Append(() => "CachingQueryRunner refreshed forced"); Cache.Remove(key); } // Run query bool fromCache = TryGetOrAdd(key, msg, out cacheValue, callbackKey => { using (CacheContext cacheContext = new CacheContext( )) { QueryResult queryResult = RunQueryImpl(query, settings, builtQuery); cacheValue = new CachingQueryRunnerValue(query, queryResult); // Add the cache context entries to the appropriate CacheInvalidator _cacheInvalidator.AddInvalidations(cacheContext, callbackKey); _cacheInvalidator.AddInvalidations(queryBuilderCacheContext, callbackKey); } return(cacheValue); }); if (fromCache && CacheContext.IsSet()) { using (CacheContext cacheContext = CacheContext.GetContext( )) { // Add the already stored changes that should invalidate this cache // entry to any outer or containing cache contexts. cacheContext.AddInvalidationsFor(_cacheInvalidator, key); } } } if (cacheValue == null) { throw new Exception("Assert false"); } // Mutate returned result to be suitable for current query QueryResult result; if (cacheValue.OriginalQuery == query) { result = cacheValue.QueryResult; } else { result = MutateResultToMatchCurrentQuery(cacheValue, query); } return(result); }
public void Test_AddInvalidationsFor(long[] entities, long[] relationshipTypes, long[] fieldTypes, long[] entityInvalidatingRelationshipTypes) { CacheInvalidator <long, long> cacheInvalidator; const int testKey = 1; IList <long> entityRefs; IList <long> relationshipTypeRefs; IList <long> fieldTypeRefs; IList <long> entityInvalidatingRelationshipTypeRefs; entityRefs = entities.ToList(); relationshipTypeRefs = relationshipTypes.ToList(); fieldTypeRefs = fieldTypes.ToList(); entityInvalidatingRelationshipTypeRefs = entityInvalidatingRelationshipTypes.ToList(); cacheInvalidator = new CacheInvalidator <long, long>(new DictionaryCache <long, long>(), "test"); using (CacheContext originalCacheContext = new CacheContext()) { originalCacheContext.Entities.Add(entityRefs); originalCacheContext.RelationshipTypes.Add(relationshipTypeRefs); originalCacheContext.FieldTypes.Add(fieldTypeRefs); originalCacheContext.EntityInvalidatingRelationshipTypes.Add(entityInvalidatingRelationshipTypeRefs); cacheInvalidator.AddInvalidations(originalCacheContext, testKey); } using (CacheContext outerCacheContext = new CacheContext()) using (CacheContext innerCacheContext = new CacheContext()) { Assert.That(innerCacheContext.Entities, Is.Empty, "Entities not initially empty"); Assert.That(innerCacheContext.RelationshipTypes, Is.Empty, "RelationshipTypes not initially empty"); Assert.That(innerCacheContext.FieldTypes, Is.Empty, "FieldTypes not initially empty"); Assert.That(innerCacheContext.EntityInvalidatingRelationshipTypes, Is.Empty, "EntityInvalidatingRelationshipTypes not initially empty"); innerCacheContext.AddInvalidationsFor(cacheInvalidator, testKey); Assert.That(innerCacheContext.Entities, Is.EquivalentTo(entityRefs).Using(EntityRefComparer.Instance), "Unexpected Entities"); Assert.That(innerCacheContext.RelationshipTypes, Is.EquivalentTo(relationshipTypeRefs).Using(EntityRefComparer.Instance), "Unexpected RelationshipTypes"); Assert.That(innerCacheContext.FieldTypes, Is.EquivalentTo(fieldTypeRefs).Using(EntityRefComparer.Instance), "Unexpected FieldTypes"); Assert.That(innerCacheContext.EntityInvalidatingRelationshipTypes, Is.EquivalentTo(entityInvalidatingRelationshipTypeRefs).Using(EntityRefComparer.Instance), "Unexpected EntityInvalidatingRelationshipTypes"); Assert.That(outerCacheContext.Entities, Is.EquivalentTo(entityRefs).Using(EntityRefComparer.Instance), "Unexpected Entities in outer cache context"); Assert.That(outerCacheContext.RelationshipTypes, Is.EquivalentTo(relationshipTypeRefs).Using(EntityRefComparer.Instance), "Unexpected RelationshipTypes in outer cache context"); Assert.That(outerCacheContext.FieldTypes, Is.EquivalentTo(fieldTypeRefs).Using(EntityRefComparer.Instance), "Unexpected FieldTypes in outer cache context"); Assert.That(outerCacheContext.EntityInvalidatingRelationshipTypes, Is.EquivalentTo(entityInvalidatingRelationshipTypeRefs).Using(EntityRefComparer.Instance), "Unexpected EntityInvalidatingRelationshipTypes in outer cache context"); } }
/// <summary> /// Build the SQL, or collect it from cache. /// </summary> /// <param name="query"></param> /// <param name="settings"></param> /// <returns></returns> public QueryBuild BuildSql(StructuredQuery query, QuerySqlBuilderSettings settings) { // Validate if (query == null) { throw new ArgumentNullException("query"); } if (QuerySqlBuilder == null) { throw new InvalidOperationException("QuerySqlBuilder not set."); } if (settings == null) { settings = new QuerySqlBuilderSettings( ); } // Check if query can even participate in cache if (!CachingQuerySqlBuilderKey.DoesRequestAllowForCaching(query, settings)) { return(BuildSqlImpl(query, settings)); } // Get a user-set key // (Users may share the same report SQL if they have the same set of read-rules) UserRuleSet userRuleSet = null; if (settings.RunAsUser != 0) { userRuleSet = UserRuleSetProvider.GetUserRuleSet(settings.RunAsUser, Permissions.Read); if (userRuleSet == null) { throw new InvalidOperationException("Expected userRuleSet"); // Assert false } } // Create cache key CachingQuerySqlBuilderKey key = new CachingQuerySqlBuilderKey(query, settings, userRuleSet); CachingQuerySqlBuilderValue cacheValue; using (MessageContext msg = new MessageContext("Reports")) { // Check for force recalculation if (settings.RefreshCachedSql) { msg.Append(() => "CachingQuerySqlBuilder refreshed forced"); _cacheInvalidator.DebugInvalidations.Add(key); Cache.Remove(key); } // In some circumstances, a result will be uncacheable, so we just return 'null' in the delegate instead. // However, on the first access, we will still be doing the calculation, so store it here. CachingQuerySqlBuilderValue calculatedOnThisAccess = null; // Check cache bool fromCache = TryGetOrAdd(key, msg, out cacheValue, callbackKey => { // This callback is called if we have a cache miss using (CacheContext cacheContext = new CacheContext( )) { QueryBuild queryResult = BuildSqlImpl(query, settings); cacheValue = new CachingQuerySqlBuilderValue(query, queryResult); calculatedOnThisAccess = cacheValue; if (queryResult.SqlIsUncacheable) { return(null); } else { // Add the cache context entries to the appropriate CacheInvalidator _cacheInvalidator.AddInvalidations(cacheContext, callbackKey); return(cacheValue); } } }); // cacheValue will be null if the result was uncacheable if (cacheValue == null) { if (calculatedOnThisAccess != null) { // In this path, the result was uncacheable, so the cache returned a 'null', // but it was the initial calculation run anyway, so we can get the value from callbackValue. cacheValue = calculatedOnThisAccess; } else { // In this path, the result was uncacheable, but someone had asked previously, and stored // the null, so we need to actually build the SQL again for this scenario. // Note: don't need to do anything with cache context, because this cache is not participating. // And if there's a parent context set, then the call to BuildSqlImpl will just talk directly to that context. QueryBuild queryResult = BuildSqlImpl(query, settings); cacheValue = new CachingQuerySqlBuilderValue(query, queryResult); } } else if (fromCache && CacheContext.IsSet( )) { // Add the already stored changes that should invalidate this cache // entry to any outer or containing cache contexts. using (CacheContext cacheContext = CacheContext.GetContext( )) { cacheContext.AddInvalidationsFor(_cacheInvalidator, key); } } } if (cacheValue == null) { throw new Exception("Assert false"); } // Mutate returned result to be suitable for current query QueryBuild result; if (cacheValue.OriginalQuery == query) { result = cacheValue.QueryResult; } else { result = MutateResultToMatchCurrentQuery(cacheValue, query); } return(result); }
/// <summary> /// Helper method to fetch multiple entries from cache, then only call the factory for missing entries. /// </summary> /// <remarks> /// Implementation notes: /// 1. The result is guaranteed to always be non-null /// 2. And contain the same entries as the keys in the same order. /// 3. All misses get evaluated in a single round /// 4. Caution: because all misses are evaluated together, they get bound together with the same invalidation. /// </remarks> /// <param name="keys"></param> /// <param name="valuesFactory"></param> /// <returns></returns> protected IReadOnlyCollection <KeyValuePair <TKey, TValue> > GetOrAddMultiple(IEnumerable <TKey> keys, Func <IEnumerable <TKey>, IEnumerable <TValue> > valuesFactory) { if (keys == null) { throw new ArgumentNullException("keys"); } if (valuesFactory == null) { throw new ArgumentNullException("valuesFactory"); } List <KeyValuePair <TKey, TValue> > result = new List <KeyValuePair <TKey, TValue> >(); KeyValuePair <TKey, TValue> resultEntry; // List of missing entries, including the index position where the result needs to be written List <Tuple <TKey, int> > cacheMisses = null; using (CacheContext cacheContext = CacheContext.IsSet() ? CacheContext.GetContext() : null) { // First pass through list int pos = 0; foreach (TKey key in keys) { TValue value; if (Cache.TryGetValue(key, out value)) { resultEntry = new KeyValuePair <TKey, TValue>(key, value); // Check cache if (cacheContext != null) { cacheContext.AddInvalidationsFor(_cacheInvalidator, key); } } else { if (cacheMisses == null) { cacheMisses = new List <Tuple <TKey, int> >(); } cacheMisses.Add(new Tuple <TKey, int>(key, pos)); // An entry still gets added to the result to maintain ordering - it will be overridden later // (can't use null .. kvp is a struct) resultEntry = new KeyValuePair <TKey, TValue>(key, default(TValue)); } result.Add(resultEntry); pos++; } } // Fill in misses if (cacheMisses != null) { using (CacheContext cacheContext = new CacheContext()) { IEnumerable <TKey> missingKeys = cacheMisses.Select(k => k.Item1); IEnumerable <TValue> missingResults = valuesFactory(missingKeys); Enumerable.Zip(cacheMisses, missingResults, (keyPos, value) => { TKey key = keyPos.Item1; int resultIndex = keyPos.Item2; resultEntry = new KeyValuePair <TKey, TValue>(key, value); // Include in current result result[resultIndex] = resultEntry; // Add to cache Cache.Add(key, value); // Add the cache context entries to the appropriate CacheInvalidator _cacheInvalidator.AddInvalidations(cacheContext, key); return((object)null); }).Last(); // call Last to force evaluate zip } } return(result); }