{"record":{"id":"25b65f8da4fab74d","repo":"elsa-workflows/elsa-core","slug":"factory-returned-null-for-cache-key-key","errorCode":null,"errorMessage":"Factory returned null for cache key: {key}.","messagePattern":"Factory returned null for cache key: (.+?)\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.Caching/Services/CacheManager.cs","lineNumber":42,"sourceCode":"    }\n    \n    /// <inheritdoc />\n    public async Task<TItem?> FindOrCreateAsync<TItem>(object key, Func<ICacheEntry, Task<TItem>> factory)\n    {\n        return await memoryCache.GetOrCreateAsync(key, async entry => await factory(entry));\n    }\n\n    /// <summary>\n    /// Retrieves a cached item by the specified key or creates a new one using the provided factory function.\n    /// </summary>\n    /// <param name=\"key\">The key used to identify the cached item.</param>\n    /// <param name=\"factory\">A factory function that provides the value to be cached if it does not already exist.</param>\n    /// <typeparam name=\"TItem\">The type of the item to retrieve or create.</typeparam>\n    /// <returns>The cached or newly created item.</returns>\n    /// <exception cref=\"InvalidOperationException\">Thrown if the factory function returns null.</exception>\n    public async Task<TItem> GetOrCreateAsync<TItem>(object key, Func<ICacheEntry, Task<TItem>> factory)\n    {\n        return await memoryCache.GetOrCreateAsync(key, async entry => await factory(entry)) ?? throw new InvalidOperationException($\"Factory returned null for cache key: {key}.\");\n    }\n}","sourceCodeStart":24,"sourceCodeEnd":44,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.Caching/Services/CacheManager.cs#L24-L44","documentation":"CacheManager.GetOrCreateAsync wraps IMemoryCache.GetOrCreateAsync and guarantees a non-null result: if the factory returns null, it throws InvalidOperationException naming the cache key. The contract is that a cached value must always be materializable; a null factory result is treated as a programming error.","triggerScenarios":"Calling GetOrCreateAsync<TItem> with a factory whose async body returns null (e.g. an entity lookup that finds nothing, or a TryGetValue-style factory).","commonSituations":"Caching a record lookup whose miss path returns null instead of a sentinel/empty value; refactoring a sync factory to async where the miss branch changed shape.","solutions":["Make the factory return a non-null value (empty collection, default record, or sentinel) on miss.","Return the error/throw a domain-specific exception inside the factory instead of returning null.","If null is legitimate, use IMemoryCache directly rather than CacheManager.GetOrCreateAsync."],"exampleFix":"// before\nvar user = await cache.GetOrCreateAsync<User>(key, async e => await repo.FindAsync(id)); // may return null\n// after\nvar user = await cache.GetOrCreateAsync<User?>(key, async e => await repo.FindAsync(id))\n           ?? await repo.FindAsync(id); // or have the factory throw/return a sentinel","handlingStrategy":"try-catch","validationCode":"var existing = memoryCache.Get(key);\n// ensure your factory cannot return null:\nTItem Load() => repo.Find(id) ?? throw new KeyNotFoundException($\"{key} not found\");","typeGuard":null,"tryCatchPattern":"try { item = await cache.GetOrCreateAsync<TItem>(key, factory); }\ncatch (InvalidOperationException ex) when (ex.Message.StartsWith(\"Factory returned null\")) { item = fallbackValue; }","preventionTips":["Ensure factories return non-null sentinels/empty collections on miss.","Throw domain exceptions from the factory instead of returning null.","Add unit tests covering cache-miss paths."],"tags":["caching","null"],"backgroundTag":"null-argument","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}