//--------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // // @owner Microsoft // @backupOwner Microsoft //--------------------------------------------------------------------- namespace System.Data.Metadata.Edm { using System; using System.Collections.Generic; using System.Data.Common.Utils; using System.Data.Entity; using System.Data.Mapping; using System.Diagnostics; using System.Runtime.Versioning; using System.Security.Permissions; using System.Threading; using System.Xml; /// /// Runtime Metadata Cache - this class contains the metadata cache entry for edm and store collections. /// internal static class MetadataCache { #region Fields private const string s_dataDirectory = "|datadirectory|"; private const string s_metadataPathSeparator = "|"; // This is the period in the periodic cleanup measured in milliseconds private const int cleanupPeriod = 5 * 60 * 1000; // This dictionary contains the cache entry for the edm item collection. The reason why we need to keep a seperate dictionary // for CSpace item collection is that the same model can be used for different providers. We don't want to load the model // again and again private static readonly Dictionary _edmLevelCache = new Dictionary(StringComparer.OrdinalIgnoreCase); /// /// This dictionary contains the store cache entry - this entry will only keep track of StorageMappingItemCollection, since internally /// storage mapping item collection keeps strong references to both edm item collection and store item collection. /// private static readonly Dictionary _storeLevelCache = new Dictionary(StringComparer.OrdinalIgnoreCase); /// /// The list maintains the store metadata entries that are still in use, maybe because someone is still holding a strong reference /// to it. We need to scan this list everytime the clean up thread wakes up and make sure if the item collection is no longer in use, /// call clear on query cache /// private static readonly List _metadataEntriesRemovedFromCache = new List(); private static Memoizer> _artifactLoaderCache = new Memoizer>(MetadataCache.SplitPaths, null); /// /// Read/Write lock for edm cache /// private static readonly object _edmLevelLock = new object(); /// /// Read/Write lock for the store cache /// private static readonly object _storeLevelLock = new object(); // Periodic thread which runs every n mins (look up the cleanupPeriod variable to see the exact time), walks through // every item in other store and edm cache and tries to do some cleanup private static Timer timer = new Timer(PeriodicCleanupCallback, null, cleanupPeriod, cleanupPeriod); #endregion #region Methods /// /// The purpose of the thread is to do cleanup. It marks the object in various stages before it actually cleans up the object /// Here's what this does for each entry in the cache: /// 1> First checks if the entry is marked for cleanup. /// 2> If the entry is marked for cleanup, that means its in one of the following 3 states /// a) If the strong reference to item collection is not null, it means that this item was marked for cleanup in /// the last cleanup cycle and we must make the strong reference set to null so that it can be garbage collected. /// b) Otherwise, we are waiting for GC to collect the item collection so that we can remove this entry from the cache /// If the weak reference to item collection is still alive, we don't do anything /// c) If the weak reference to item collection is not alive, we need to remove this entry from the cache /// 3> If the entry is not marked for cleanup, then check whether the weak reference to entry token is alive /// a) if it is alive, then this entry is in use and we must do nothing /// b) Otherwise, we can mark this entry for cleanup /// /// private static void PeriodicCleanupCallback(object state) { // Perform clean up on edm cache DoCacheClean(_edmLevelCache, _edmLevelLock); // Perform clean up on store cache DoCacheClean(_storeLevelCache, _storeLevelLock); } /// /// A helper function for splitting up a string that is a concatenation of strings delimited by the metadata /// path separator into a string list. The resulting list is NOT sorted. /// /// The paths to split /// An array of strings [ResourceExposure(ResourceScope.Machine)] //Exposes the file name which is a Machine resource [ResourceConsumption(ResourceScope.Machine)] //For MetadataArtifactLoader.Create method call. But the path is not created in this method. internal static List SplitPaths(string paths) { Debug.Assert(!string.IsNullOrEmpty(paths), "paths cannot be empty or null"); string[] results; // This is the registry of all URIs in the global collection. HashSet uriRegistry = new HashSet(StringComparer.OrdinalIgnoreCase); List loaders = new List(); // If the argument contains one or more occurrences of the macro '|DataDirectory|', we // pull those paths out so that we don't lose them in the string-splitting logic below. // Note that the macro '|DataDirectory|' cannot have any whitespace between the pipe // symbols and the macro name. Also note that the macro must appear at the beginning of // a path (else we will eventually fail with an invalid path exception, because in that // case the macro is not expanded). If a real/physical folder named 'DataDirectory' needs // to be included in the metadata path, whitespace should be used on either or both sides // of the name. // List dataDirPaths = new List(); int indexStart = paths.IndexOf(MetadataCache.s_dataDirectory, StringComparison.OrdinalIgnoreCase); while (indexStart != -1) { int prevSeparatorIndex = indexStart == 0 ? -1 : paths.LastIndexOf( MetadataCache.s_metadataPathSeparator, indexStart - 1, // start looking here StringComparison.Ordinal ); int macroPathBeginIndex = prevSeparatorIndex + 1; // The '|DataDirectory|' macro is composable, so identify the complete path, like // '|DataDirectory|\item1\item2'. If the macro appears anywhere other than at the // beginning, splice out the entire path, e.g. 'C:\item1\|DataDirectory|\item2'. In this // latter case the macro will not be expanded, and downstream code will throw an exception. // int indexEnd = paths.IndexOf(MetadataCache.s_metadataPathSeparator, indexStart + MetadataCache.s_dataDirectory.Length, StringComparison.Ordinal); if (indexEnd == -1) { dataDirPaths.Add(paths.Substring(macroPathBeginIndex)); paths = paths.Remove(macroPathBeginIndex); // update the concatenated list of paths break; } dataDirPaths.Add(paths.Substring(macroPathBeginIndex, indexEnd - macroPathBeginIndex)); // Update the concatenated list of paths by removing the one containing the macro. // paths = paths.Remove(macroPathBeginIndex, indexEnd - macroPathBeginIndex); indexStart = paths.IndexOf(MetadataCache.s_dataDirectory, StringComparison.OrdinalIgnoreCase); } // Split the string on the separator and remove all spaces around each parameter value results = paths.Split(new string[] { MetadataCache.s_metadataPathSeparator }, StringSplitOptions.RemoveEmptyEntries); // Now that the non-macro paths have been identified, merge the paths containing the macro // into the complete list. // if (dataDirPaths.Count > 0) { dataDirPaths.AddRange(results); results = dataDirPaths.ToArray(); } for (int i = 0; i < results.Length; i++) { // Trim out all the spaces for this parameter and add it only if it's not blank results[i] = results[i].Trim(); if (results[i].Length > 0) { loaders.Add(MetadataArtifactLoader.Create( results[i], MetadataArtifactLoader.ExtensionCheck.All, // validate the extension against all acceptable values null, uriRegistry )); } } return loaders; } /// /// Walks through the given cache and calls cleanup on each entry in the cache /// /// /// /// private static void DoCacheClean(Dictionary cache, object objectToLock) where T: MetadataEntry { // Sometime, for some reason, timer can be initialized and the cache is still not initialized. if (cache != null) { List> keysForRemoval = null; lock (objectToLock) { // we should check for type of the lock object first, since otherwise we might be reading the count of the list // while some other thread might be modifying it. For e.g. when this function is called for edmcache, // we will be acquiring edmlock and trying to get the count for the list, while some other thread // might be calling ClearCache and we might be adding entries to the list if (objectToLock == _storeLevelLock && _metadataEntriesRemovedFromCache.Count != 0) { // First check the list of entries and remove things which are no longer in use for (int i = _metadataEntriesRemovedFromCache.Count - 1; 0 <= i; i--) { if (!_metadataEntriesRemovedFromCache[i].IsEntryStillValid()) { // Clear the query cache _metadataEntriesRemovedFromCache[i].CleanupQueryCache(); // Remove the entry at the current index. This is the reason why we // go backwards. _metadataEntriesRemovedFromCache.RemoveAt(i); } } } // We have to use a list to keep track of the keys to remove because we can't remove while enumerating foreach (KeyValuePair pair in cache) { if (pair.Value.PeriodicCleanUpThread()) { if (keysForRemoval == null) { keysForRemoval = new List>(); } keysForRemoval.Add(pair); } } // Remove all the entries from the cache if (keysForRemoval != null) { for (int i = 0; i < keysForRemoval.Count; i++) { keysForRemoval[i].Value.Clear(); cache.Remove(keysForRemoval[i].Key); } } } } } /// /// Retrieves an cache entry holding to edm metadata for a given cache key /// /// string containing all the files from which edm metadata is to be retrieved /// An instance of the composite MetadataArtifactLoader /// The metadata entry token for the returned entry /// Returns the entry containing the edm metadata internal static EdmItemCollection GetOrCreateEdmItemCollection(string cacheKey, MetadataArtifactLoader loader, out object entryToken) { EdmMetadataEntry entry = GetCacheEntry(_edmLevelCache, cacheKey, _edmLevelLock, new EdmMetadataEntryConstructor(), out entryToken); // Load the edm item collection or if the collection is already loaded, check for security permission LoadItemCollection(new EdmItemCollectionLoader(loader), entry); return entry.EdmItemCollection; } /// /// Retrieves an entry holding store metadata for a given cache key /// /// The connection string whose store metadata is to be retrieved /// An instance of the composite MetadataArtifactLoader /// The metadata entry token for the returned entry /// the entry containing the information on how to load store metadata internal static StorageMappingItemCollection GetOrCreateStoreAndMappingItemCollections( string cacheKey, MetadataArtifactLoader loader, EdmItemCollection edmItemCollection, out object entryToken) { StoreMetadataEntry entry = GetCacheEntry(_storeLevelCache, cacheKey, _storeLevelLock, new StoreMetadataEntryConstructor(), out entryToken); // Load the store item collection or if the collection is already loaded, check for security permission LoadItemCollection(new StoreItemCollectionLoader(edmItemCollection, loader), entry); return entry.StorageMappingItemCollection; } internal static List GetOrCreateMetdataArtifactLoader(string paths) { return _artifactLoaderCache.Evaluate(paths); } /// /// Get the entry from the cache given the cache key. If the entry is not present, it creates a new entry and /// adds it to the cache /// /// /// /// /// /// /// /// private static T GetCacheEntry(Dictionary cache, string cacheKey, object objectToLock, IMetadataEntryConstructor metadataEntry, out object entryToken) where T: MetadataEntry { T entry; // In the critical section, we need to do the minimal thing to ensure correctness // Within the lock, we will see if an entry is present. If it is not, we will create a new entry and // add it to the cache. In either case, we need to ensure the token to make sure so that any other // thread that comes looking for the same entry does nothing in this critical section // Also the cleanup thread doesn't do anything since the token is alive lock (objectToLock) { if (cache.TryGetValue(cacheKey, out entry)) { entryToken = entry.EnsureToken(); } else { entry = metadataEntry.GetMetadataEntry(); entryToken = entry.EnsureToken(); cache.Add(cacheKey, entry); } } return entry; } /// /// Loads the item collection for the entry /// /// struct which loads an item collection /// entry whose item collection needs to be loaded private static void LoadItemCollection(IItemCollectionLoader itemCollectionLoader, T entry) where T : MetadataEntry { // At this point, you have made sure that there is an entry with an alive token in the cache so that // other threads can find it if they come querying for it, and cleanup thread won't clean the entry // If two or more threads come one after the other, we don't won't both of them to load the metadata. // So if one of them is loading the metadata, the other should wait and then use the same metadata. // For that reason, we have this lock on the entry itself to make sure that this happens. Its okay to // update the item collection outside the lock, since assignment are guarantees to be atomic and no two // thread are updating this at the same time bool isItemCollectionAlreadyLoaded = true; if (!entry.IsLoaded) { lock (entry) { if (!entry.IsLoaded) { itemCollectionLoader.LoadItemCollection(entry); isItemCollectionAlreadyLoaded = false; } } } Debug.Assert(entry.IsLoaded, "The entry must be loaded at this point"); // Making sure that the thread which loaded the item collection is not checking for file permisssions // again if (isItemCollectionAlreadyLoaded) { entry.CheckFilePermission(); } } /// /// Remove all the entries from the cache /// internal static void Clear() { lock (_edmLevelLock) { _edmLevelCache.Clear(); } lock (_storeLevelLock) { // Call clear on each of the metadata entries. This is to make sure we clear all the performance // counters associated with the query cache foreach (StoreMetadataEntry entry in _storeLevelCache.Values) { // Check if the weak reference to item collection is still alive if (entry.IsEntryStillValid()) { _metadataEntriesRemovedFromCache.Add(entry); } else { entry.Clear(); } } _storeLevelCache.Clear(); } Memoizer> artifactLoaderCacheTemp = new Memoizer>(MetadataCache.SplitPaths, null); Interlocked.CompareExchange(ref _artifactLoaderCache, artifactLoaderCacheTemp, _artifactLoaderCache); } #endregion #region InlineClasses /// /// The base class having common implementation for all metadata entry classes /// private abstract class MetadataEntry { private WeakReference _entryTokenReference; private ItemCollection _itemCollection; private WeakReference _weakReferenceItemCollection; private bool _markEntryForCleanup; private FileIOPermission _filePermissions; /// /// The constructor for constructing this MetadataEntry /// internal MetadataEntry() { // Create this once per life time of the object. Creating extra weak references causing unnecessary GC pressure _entryTokenReference = new WeakReference(null); _weakReferenceItemCollection = new WeakReference(null); } /// /// returns the item collection inside this metadata entry /// protected ItemCollection ItemCollection { get { return _itemCollection; } } /// /// Update the entry with the given item collection /// /// protected void UpdateMetadataEntry(ItemCollection itemCollection, FileIOPermission filePermissions) { Debug.Assert(_entryTokenReference.IsAlive, "You must call Ensure token before you call this method"); Debug.Assert(_markEntryForCleanup == false, "The entry must not be marked for cleanup"); Debug.Assert(_itemCollection == null, "Item collection must be null"); Debug.Assert(_filePermissions == null, "filePermissions must be null"); // Update strong and weak reference for item collection _weakReferenceItemCollection.Target = itemCollection; _filePermissions = filePermissions; // do this last, because it signals that we are loaded _itemCollection = itemCollection; } internal bool IsLoaded { get { return _itemCollection != null; } } /// /// This method is called periodically by the cleanup thread to make the unused entries /// go through various stages, before it is ready for cleanup. If it is ready, this method /// returns true and then the entry is completely removed from the cache /// /// internal bool PeriodicCleanUpThread() { // Here's what this does for each entry in the cache: // 1> First checks if the entry is marked for cleanup. // 2> If the entry is marked for cleanup, that means its in one of the following 3 states // a) If the strong reference to item collection is not null, it means that this item was marked for cleanup in // the last cleanup cycle and we must make the strong reference set to null so that it can be garbage collected. (GEN 2) // b) Otherwise, we are waiting for GC to collect the item collection so that we can remove this entry from the cache // If the weak reference to item collection is still alive, we don't do anything // c) If the weak reference to item collection is not alive, we need to remove this entry from the cache (GEN 3) // 3> If the entry is not marked for cleanup, then check whether the weak reference to entry token is alive // a) if it is alive, then this entry is in use and we must do nothing // b) Otherwise, we can mark this entry for cleanup (GEN 1) if (_markEntryForCleanup) { Debug.Assert(_entryTokenReference.IsAlive == false, "Entry Token must never be alive if the entry is marked for cleanup"); if (_itemCollection != null) { // GEN 2 _itemCollection = null; } else if (!_weakReferenceItemCollection.IsAlive) { // GEN 3 _filePermissions = null; // this entry must be removed from the cache return true; } } else if (!_entryTokenReference.IsAlive) { // GEN 1 // If someone creates a entity connection, and calls GetMetadataWorkspace. This creates an cache entry, // but the item collection is not initialized yet (since store item collection are initialized only // when one calls connection.Open()). Suppose now the connection is no longer used - in other words, // open was never called and it goes out of scope. After some time when the connection gets GC'ed, // entry token won't be alive any longer, but item collection inside it will be null, since it was never initialized. // So we can't assert that item collection must be always initialized here _markEntryForCleanup = true; } return false; } /// /// Make sure that the entry has a alive token and returns that token - it can be new token or an existing /// one, depending on the state of the entry /// /// internal object EnsureToken() { object entryToken = _entryTokenReference.Target; ItemCollection itemCollection = (ItemCollection)_weakReferenceItemCollection.Target; // When ensure token is called, the entry can be in different stages // 1> Its a newly created entry - no token, no item collection, etc. Just create a new token and // return back // 2> An entry already in use - the weak reference to token must be alive. We just need to grab the token // and return it // 3> No one is using this entry and hence the token is no longer alive. If we have strong reference to item // collection, then create a new token and return it // 4> No one has used this token for one cleanup cycle and hence strong reference is null. But the weak reference // is still alive. We need to make the initialize the strong reference again, create a new token and return it // 5> This entry has not been used for long enough that even the weak reference is no longer alive. This entry is // now exactly like a new entry, except that it is still marked for cleanup. Create a new token, set mark for // cleanup to false and return the token if (_entryTokenReference.IsAlive) { Debug.Assert(_markEntryForCleanup == false, "An entry with alive token cannot be marked for cleanup"); // ItemCollection strong pointer can be null or not null. If the entry has been created, and loadItemCollection // hasn't been called yet, the token will be alive, but item collection will be null. If someone called // load item collection, then item collection will not be non-null return entryToken; } // If the entry token is not alive, then it can be either a new created entry with everything set // to null or it must be one of the entries which is no longer in use else if (_itemCollection != null) { Debug.Assert(_weakReferenceItemCollection.IsAlive, "Since the strong reference is still there, weak reference must also be alive"); // This means that no one is using the item collection, and its waiting to be cleanuped } else { if (_weakReferenceItemCollection.IsAlive) { Debug.Assert(_markEntryForCleanup, "Since the strong reference is null, this entry must be marked for cleanup"); // Initialize the strong reference to item collection _itemCollection = itemCollection; } else { // no more references to the collection // are available, so get rid of the permissions // object. We will get a new one when we get a new collection _filePermissions = null; } } // Even if the _weakReferenceItemCollection is no longer alive, we will reuse this entry. Assign a new entry token and set mark for cleanup to false // so that this entry is not cleared by the cleanup thread entryToken = new object(); _entryTokenReference.Target = entryToken; _markEntryForCleanup = false; return entryToken; } /// /// Check if the thread has appropriate permissions to use the already loaded metadata /// internal void CheckFilePermission() { Debug.Assert(_itemCollection != null, "Item collection must be present since we want to reuse the metadata"); Debug.Assert(_entryTokenReference.IsAlive, "This entry must be in use"); Debug.Assert(_markEntryForCleanup == false, "The entry must not marked for cleanup"); Debug.Assert(_weakReferenceItemCollection.IsAlive, "Weak reference to item collection must be alive"); // we will have an empty ItemCollection (no files were used to load it) if (_filePermissions != null) { _filePermissions.Demand(); } } /// /// Dispose the composite loader that encapsulates all artifacts /// internal virtual void Clear() { } /// /// This returns true if the entry is still in use - the entry can be use if the entry token is /// still alive.If the entry token is still not alive, it means that no one is using this entry /// and its okay to remove it. Today there is no /// /// internal bool IsEntryStillValid() { return _entryTokenReference.IsAlive; } } /// /// A metadata entry holding EdmItemCollection object for the cache /// private class EdmMetadataEntry : MetadataEntry { /// /// Gets the EdmItemCollection for this entry /// internal EdmItemCollection EdmItemCollection { get { return (EdmItemCollection)this.ItemCollection; } } /// /// Just loads the edm item collection /// /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2103:ReviewImperativeSecurity")] internal void LoadEdmItemCollection(MetadataArtifactLoader loader) { Debug.Assert(loader != null, "loader is null"); List readers = loader.CreateReaders(DataSpace.CSpace); try { EdmItemCollection itemCollection = new EdmItemCollection( readers, loader.GetPaths(DataSpace.CSpace) ); List permissionPaths = new List(); loader.CollectFilePermissionPaths(permissionPaths, DataSpace.CSpace); FileIOPermission filePermissions = null; if (permissionPaths.Count > 0) { filePermissions = new FileIOPermission(FileIOPermissionAccess.Read, permissionPaths.ToArray()); } UpdateMetadataEntry(itemCollection, filePermissions); } finally { Helper.DisposeXmlReaders(readers); } } } /// /// A metadata entry holding a StoreItemCollection and a StorageMappingItemCollection objects for the cache /// private class StoreMetadataEntry : MetadataEntry { private System.Data.Common.QueryCache.QueryCacheManager _queryCacheManager; /// /// The constructor for constructing this entry with an StoreItemCollection and a StorageMappingItemCollection /// /// An instance of the composite MetadataArtifactLoader internal StoreMetadataEntry() { } /// /// Gets the StorageMappingItemCollection for this entry /// internal StorageMappingItemCollection StorageMappingItemCollection { get { return (StorageMappingItemCollection)this.ItemCollection; } } /// /// Load store specific metadata into the StoreItemCollection for this entry /// /// The store-specific provider factory /// edmItemCollection [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2103:ReviewImperativeSecurity")] internal void LoadStoreCollection(EdmItemCollection edmItemCollection, MetadataArtifactLoader loader) { StoreItemCollection storeItemCollection = null; IEnumerable sSpaceXmlReaders = loader.CreateReaders(DataSpace.SSpace); try { // Load the store side, however, only do so if we don't already have one storeItemCollection = new StoreItemCollection( sSpaceXmlReaders, loader.GetPaths(DataSpace.SSpace)); } finally { Helper.DisposeXmlReaders(sSpaceXmlReaders); } // If this entry is getting re-used, make sure that the previous query cache manager gets // cleared up if (_queryCacheManager != null) { _queryCacheManager.Clear(); } // Update the query cache manager reference _queryCacheManager = storeItemCollection.QueryCacheManager; // With the store metadata in place, we can then load the mappings, however, only use it // if we don't already have one // StorageMappingItemCollection storageMappingItemCollection = null; IEnumerable csSpaceXmlReaders = loader.CreateReaders(DataSpace.CSSpace); try { storageMappingItemCollection = new StorageMappingItemCollection( edmItemCollection, storeItemCollection, csSpaceXmlReaders, loader.GetPaths(DataSpace.CSSpace)); } finally { Helper.DisposeXmlReaders(csSpaceXmlReaders); } List permissionPaths = new List(); loader.CollectFilePermissionPaths(permissionPaths, DataSpace.SSpace); loader.CollectFilePermissionPaths(permissionPaths, DataSpace.CSSpace); FileIOPermission filePermissions = null; if (permissionPaths.Count > 0) { filePermissions = new FileIOPermission(FileIOPermissionAccess.Read, permissionPaths.ToArray()); } this.UpdateMetadataEntry(storageMappingItemCollection, filePermissions); } /// /// Calls clear on query cache manager to make sure all the performance counters associated with the query /// cache are gone /// internal override void Clear() { // there can be entries in cache for which the store item collection was never created. For e.g. // if you create a new entity connection, but never call open on it CleanupQueryCache(); base.Clear(); } /// /// Cleans and Dispose query cache manager /// internal void CleanupQueryCache() { if (null != _queryCacheManager) { _queryCacheManager.Dispose(); _queryCacheManager = null; } } } /// /// Interface to construct the metadata entry so that code can be reused /// /// interface IMetadataEntryConstructor { T GetMetadataEntry(); } /// /// Struct for creating EdmMetadataEntry /// private struct EdmMetadataEntryConstructor : IMetadataEntryConstructor { public EdmMetadataEntry GetMetadataEntry() { return new EdmMetadataEntry(); } } /// /// Struct for creating StoreMetadataEntry /// private struct StoreMetadataEntryConstructor : IMetadataEntryConstructor { public StoreMetadataEntry GetMetadataEntry() { return new StoreMetadataEntry(); } } /// /// Interface which constructs a new Item collection /// /// interface IItemCollectionLoader where T : MetadataEntry { void LoadItemCollection(T entry); } private struct EdmItemCollectionLoader : IItemCollectionLoader { private MetadataArtifactLoader _loader; public EdmItemCollectionLoader(MetadataArtifactLoader loader) { Debug.Assert(loader != null, "loader must never be null"); _loader = loader; } /// /// Creates a new item collection and updates the entry with the item collection /// /// /// public void LoadItemCollection(EdmMetadataEntry entry) { entry.LoadEdmItemCollection(_loader); } } private struct StoreItemCollectionLoader : IItemCollectionLoader { private EdmItemCollection _edmItemCollection; private MetadataArtifactLoader _loader; /// /// Constructs a struct from which you can load edm item collection /// /// /// internal StoreItemCollectionLoader(EdmItemCollection edmItemCollection, MetadataArtifactLoader loader) { Debug.Assert(edmItemCollection != null, "EdmItemCollection must never be null"); Debug.Assert(loader != null, "loader must never be null"); //StoreItemCollection requires atleast one SSDL path. if ((loader.GetPaths(DataSpace.SSpace) == null) || (loader.GetPaths(DataSpace.SSpace).Count == 0)) { throw EntityUtil.Metadata(Strings.AtleastOneSSDLNeeded); } _edmItemCollection = edmItemCollection; _loader = loader; } public void LoadItemCollection(StoreMetadataEntry entry) { entry.LoadStoreCollection(_edmItemCollection, _loader); } } #endregion } }