diff --git a/src/EntityFramework.MappingAPI/EfMap.cs b/src/EntityFramework.MappingAPI/EfMap.cs index ab24b6b..258ed11 100644 --- a/src/EntityFramework.MappingAPI/EfMap.cs +++ b/src/EntityFramework.MappingAPI/EfMap.cs @@ -1,6 +1,6 @@ using EntityFramework.MappingAPI.Mappings; using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Data.Entity; using System.Data.Entity.Infrastructure; @@ -12,9 +12,14 @@ namespace EntityFramework.MappingAPI internal class EfMap { /// - /// + /// Mappings are cached per model for the lifetime of the process. A concurrent dictionary is + /// required, not just convenient: BulkInsert is reachable from several threads at once (any + /// app that bulk inserts from concurrent jobs or requests), and a plain Dictionary being + /// written on one thread while another reads it can throw or return the wrong entry. The + /// caller then sees an unrelated failure - via MappedDataReader, which catches per entity + /// type, that surfaces as the misleading "No table mappings provided." /// - private static readonly Dictionary Mappings = new Dictionary(); + private static readonly ConcurrentDictionary Mappings = new ConcurrentDictionary(); /// /// @@ -60,14 +65,11 @@ public static DbMapping Get(DbContext context) cacheKey = iDbModelCacheKeyProvider.CacheKey; } - DbMapping mapping; - if (Mappings.TryGetValue(cacheKey, out mapping)) - return mapping; - - mapping = new DbMapping(context); - - Mappings[cacheKey] = mapping; - return mapping; + // GetOrAdd rather than TryGetValue-then-assign: two threads racing here used to corrupt + // the cache. They may still both build a DbMapping on first use and one result is + // discarded, which is wasteful but correct - and it keeps a failed build from being + // cached, which a Lazy would not. + return Mappings.GetOrAdd(cacheKey, _ => new DbMapping(context)); } } }