//--------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // // @owner [....] // @backupOwner [....] //--------------------------------------------------------------------- using System.Collections.Generic; using System.Configuration; using System.Data.Common; using System.Data.Common.CommandTrees; using System.Data.Common.EntitySql; using System.Data.Entity; using System.Data.Mapping; using System.Data.Metadata.Edm; using System.Diagnostics; using System.Linq; using System.Runtime.Versioning; using System.Text; using System.Transactions; namespace System.Data.EntityClient { /// /// Class representing a connection for the conceptual layer. An entity connection may only /// be initialized once (by opening the connection). It is subsequently not possible to change /// the connection string, attach a new store connection, or change the store connection string. /// public sealed class EntityConnection : DbConnection { #region Constants private const string s_metadataPathSeparator = "|"; private const string s_semicolonSeparator = ";"; private const string s_entityClientProviderName = "System.Data.EntityClient"; private const string s_providerInvariantName = "provider"; private const string s_providerConnectionString = "provider connection string"; private const string s_readerPrefix = "reader://"; internal static readonly StateChangeEventArgs StateChangeClosed = new StateChangeEventArgs(ConnectionState.Open, ConnectionState.Closed); internal static readonly StateChangeEventArgs StateChangeOpen = new StateChangeEventArgs(ConnectionState.Closed, ConnectionState.Open); #endregion private readonly object _connectionStringLock = new object(); private static readonly DbConnectionOptions s_emptyConnectionOptions = new DbConnectionOptions(String.Empty, null); // The connection options object having the connection settings needed by this connection private DbConnectionOptions _userConnectionOptions; private DbConnectionOptions _effectiveConnectionOptions; // The internal connection state of the entity client, which is distinct from that of the // store connection it aggregates. private ConnectionState _entityClientConnectionState = ConnectionState.Closed; private DbProviderFactory _providerFactory; private DbConnection _storeConnection; private readonly bool _userOwnsStoreConnection; private MetadataWorkspace _metadataWorkspace; // DbTransaction started using BeginDbTransaction() method private EntityTransaction _currentTransaction; // Transaction the user enlisted in using EnlistTransaction() method private Transaction _enlistedTransaction; private bool _initialized; // will only have a value while waiting for the ssdl to be loaded. we should // never have a value for this when _initialized == true private MetadataArtifactLoader _artifactLoader; /// /// Constructs the EntityConnection object with a connection not yet associated to a particular store /// [ResourceExposure(ResourceScope.None)] //We are not exposing any resource [ResourceConsumption(ResourceScope.Machine, ResourceScope.Machine)] //For EntityConnection constructor. But since the connection string we pass in is an Empty String, //we consume the resource and do not expose it any further. public EntityConnection() : this(String.Empty) { } /// /// Constructs the EntityConnection object with a connection string /// /// The connection string, may contain a list of settings for the connection or /// just the name of the connection to use [ResourceExposure(ResourceScope.Machine)] //Exposes the file names as part of ConnectionString which are a Machine resource [ResourceConsumption(ResourceScope.Machine)] //For ChangeConnectionString method call. But the paths are not created in this method. public EntityConnection(string connectionString) { GC.SuppressFinalize(this); this.ChangeConnectionString(connectionString); } /// /// Constructs the EntityConnection from Metadata loaded in memory /// /// Workspace containing metadata information. public EntityConnection(MetadataWorkspace workspace, DbConnection connection) { GC.SuppressFinalize(this); EntityUtil.CheckArgumentNull(workspace, "workspace"); EntityUtil.CheckArgumentNull(connection, "connection"); if (!workspace.IsItemCollectionAlreadyRegistered(DataSpace.CSpace)) { throw EntityUtil.Argument(System.Data.Entity.Strings.EntityClient_ItemCollectionsNotRegisteredInWorkspace("EdmItemCollection")); } if(!workspace.IsItemCollectionAlreadyRegistered(DataSpace.SSpace)) { throw EntityUtil.Argument(System.Data.Entity.Strings.EntityClient_ItemCollectionsNotRegisteredInWorkspace("StoreItemCollection")); } if(!workspace.IsItemCollectionAlreadyRegistered(DataSpace.CSSpace)) { throw EntityUtil.Argument(System.Data.Entity.Strings.EntityClient_ItemCollectionsNotRegisteredInWorkspace("StorageMappingItemCollection")); } if (connection.State != ConnectionState.Closed) { throw EntityUtil.Argument(System.Data.Entity.Strings.EntityClient_ConnectionMustBeClosed); } // Verify that a factory can be retrieved if (DbProviderFactories.GetFactory(connection) == null) { throw EntityUtil.ProviderIncompatible(System.Data.Entity.Strings.EntityClient_DbConnectionHasNoProvider(connection)); } StoreItemCollection collection = (StoreItemCollection)workspace.GetItemCollection(DataSpace.SSpace); _providerFactory = collection.StoreProviderFactory; _storeConnection = connection; _userOwnsStoreConnection = true; _metadataWorkspace = workspace; _initialized = true; } /// /// Get or set the entity connection string associated with this connection object /// public override string ConnectionString { get { //EntityConnection created using MetadataWorkspace // _userConnectionOptions is not null when empty Constructor is used // Therefore it is sufficient to identify whether EC(MW, DbConnection) is used if (this._userConnectionOptions == null) { Debug.Assert(_storeConnection != null); string invariantName; if (!EntityUtil.TryGetProviderInvariantName(DbProviderFactories.GetFactory(_storeConnection), out invariantName)) { Debug.Fail("Provider Invariant Name not found"); invariantName = ""; } return string.Format(Globalization.CultureInfo.InvariantCulture, "{0}={3}{4};{1}={5};{2}=\"{6}\";", EntityConnectionStringBuilder.MetadataParameterName, s_providerInvariantName, s_providerConnectionString, s_readerPrefix, _metadataWorkspace.MetadataWorkspaceId, invariantName, FormatProviderString(_storeConnection.ConnectionString)); } string userConnectionString = this._userConnectionOptions.UsersConnectionString; // In here, we ask the store connection for the connection string only if the user didn't specify a name // connection (meaning effective connection options == user connection options). If the user specified a // named connection, then we return just that. Otherwise, if the connection string is different from what // we have in the connection options, which is possible if the store connection changed the connection // string to hide the password, then we use the builder to reconstruct the string. The parameters will be // shuffled, which is unavoidable but it's ok because the connection string cannot be the same as what the // user originally passed in anyway. However, if the store connection string is still the same, then we // simply return what the user originally passed in. if (object.ReferenceEquals(_userConnectionOptions, _effectiveConnectionOptions) && this._storeConnection != null) { string storeConnectionString = null; try { storeConnectionString = this._storeConnection.ConnectionString; } catch (Exception e) { if (EntityUtil.IsCatchableExceptionType(e)) { throw EntityUtil.Provider(@"ConnectionString", e); } throw; } // SQLBU 514721, 515024 - Defer connection string parsing to ConnectionStringBuilder // if the 'userStoreConnectionString' and 'storeConnectionString' are unequal, except // when they are both null or empty (we treat null and empty as equivalent here). // string userStoreConnectionString = this._userConnectionOptions[EntityConnectionStringBuilder.ProviderConnectionStringParameterName]; if ((storeConnectionString != userStoreConnectionString) && !(string.IsNullOrEmpty(storeConnectionString) && string.IsNullOrEmpty(userStoreConnectionString))) { // Feeds the connection string into the connection string builder, then plug in the provider connection string into // the builder, and then extract the string from the builder EntityConnectionStringBuilder connectionStringBuilder = new EntityConnectionStringBuilder(userConnectionString); connectionStringBuilder.ProviderConnectionString = storeConnectionString; return connectionStringBuilder.ConnectionString; } } return userConnectionString; } [ResourceExposure(ResourceScope.Machine)] //Exposes the file names as part of ConnectionString which are a Machine resource [ResourceConsumption(ResourceScope.Machine)] //For ChangeConnectionString method call. But the paths are not created in this method. set { ValidateChangesPermitted(); this.ChangeConnectionString(value); } } /// /// Formats provider string to replace " with \" so it can be appended within quotation marks "..." /// private static string FormatProviderString(string providerString) { return providerString.Trim().Replace("\"", "\\\""); } /// /// Get the time to wait when attempting to establish a connection before ending the try and generating an error /// public override int ConnectionTimeout { get { if (this._storeConnection == null) return 0; try { return this._storeConnection.ConnectionTimeout; } catch (Exception e) { if (EntityUtil.IsCatchableExceptionType(e)) { throw EntityUtil.Provider(@"ConnectionTimeout", e); } throw; } } } /// /// Get the name of the current database or the database that will be used after a connection is opened /// public override string Database { get { return String.Empty; } } /// /// Gets the ConnectionState property of the EntityConnection /// public override ConnectionState State { get { try { if (this._entityClientConnectionState == ConnectionState.Open) { Debug.Assert(this.StoreConnection != null); if (this.StoreConnection.State != ConnectionState.Open) { return ConnectionState.Broken; } } return this._entityClientConnectionState; } catch (Exception e) { if (EntityUtil.IsCatchableExceptionType(e)) { throw EntityUtil.Provider(@"State", e); } throw; } } } /// /// Gets the name or network address of the data source to connect to /// public override string DataSource { get { if (this._storeConnection == null) return String.Empty; try { return this._storeConnection.DataSource; } catch (Exception e) { if (EntityUtil.IsCatchableExceptionType(e)) { throw EntityUtil.Provider(@"DataSource", e); } throw; } } } /// /// Gets a string that contains the version of the data store to which the client is connected /// public override string ServerVersion { get { if (this._storeConnection == null) throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_ConnectionStringNeededBeforeOperation); if (this.State != ConnectionState.Open) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_ConnectionNotOpen); } try { return this._storeConnection.ServerVersion; } catch (Exception e) { if (EntityUtil.IsCatchableExceptionType(e)) { throw EntityUtil.Provider(@"ServerVersion", e); } throw; } } } /// /// Gets the provider factory associated with EntityConnection /// override protected DbProviderFactory DbProviderFactory { get { return EntityProviderFactory.Instance; } } /// /// Gets the DbProviderFactory for the underlying provider /// internal DbProviderFactory StoreProviderFactory { get { return this._providerFactory; } } /// /// Gets the DbConnection for the underlying provider connection /// public DbConnection StoreConnection { get { return this._storeConnection; } } /// /// Gets the metadata workspace used by this connection /// [CLSCompliant(false)] public MetadataWorkspace GetMetadataWorkspace() { return GetMetadataWorkspace(true /* initializeAllCollections */); } private bool ShouldRecalculateMetadataArtifactLoader(List loaders) { if (loaders.Any(loader => loader.GetType() == typeof(MetadataArtifactLoaderCompositeFile))) { // the loaders had folders in it return true; } // in the case that loaders only contains resources or file name, we trust the cache return false; } [ResourceExposure(ResourceScope.None)] //The resource( path name) is not exposed to the callers of this method [ResourceConsumption(ResourceScope.Machine, ResourceScope.Machine)] //Fir SplitPaths call and we pick the file names from class variable. internal MetadataWorkspace GetMetadataWorkspace(bool initializeAllCollections) { Debug.Assert(_metadataWorkspace != null || _effectiveConnectionOptions != null, "The effective connection options is null, which should never be"); if (_metadataWorkspace == null || (initializeAllCollections && !_metadataWorkspace.IsItemCollectionAlreadyRegistered(DataSpace.SSpace))) { // This lock is to ensure that the connection string and the metadata workspace are in a consistent state, that is, you // don't get a metadata workspace not matching what's described by the connection string lock (_connectionStringLock) { EdmItemCollection edmItemCollection = null; if (_metadataWorkspace == null) { MetadataWorkspace workspace = new MetadataWorkspace(); List loaders = new List(); string paths = _effectiveConnectionOptions[EntityConnectionStringBuilder.MetadataParameterName]; if (!string.IsNullOrEmpty(paths)) { loaders = MetadataCache.GetOrCreateMetdataArtifactLoader(paths); if(!ShouldRecalculateMetadataArtifactLoader(loaders)) { _artifactLoader = MetadataArtifactLoader.Create(loaders); } else { // the loaders contains folders that might get updated during runtime, so we have to recalculate the loaders again _artifactLoader = MetadataArtifactLoader.Create(MetadataCache.SplitPaths(paths)); } } else { _artifactLoader = MetadataArtifactLoader.Create(loaders); } edmItemCollection = LoadEdmItemCollection(workspace, _artifactLoader); _metadataWorkspace = workspace; } else { edmItemCollection = (EdmItemCollection)_metadataWorkspace.GetItemCollection(DataSpace.CSpace); } if (initializeAllCollections && !_metadataWorkspace.IsItemCollectionAlreadyRegistered(DataSpace.SSpace)) { LoadStoreItemCollections(_metadataWorkspace, _storeConnection, _providerFactory, _effectiveConnectionOptions, edmItemCollection, _artifactLoader); _artifactLoader = null; _initialized = true; } } } return _metadataWorkspace; } /// /// Gets the current transaction that this connection is enlisted in /// internal EntityTransaction CurrentTransaction { get { // Null out the current transaction if the state is closed or zombied if ((null != _currentTransaction) && ((null == _currentTransaction.StoreTransaction.Connection) || (this.State == ConnectionState.Closed))) { ClearCurrentTransaction(); } return _currentTransaction; } } /// /// Whether the user has enlisted in transaction using EnlistTransaction method /// /// /// To avoid threading issues the field is not reset when the transaction is completed. /// Therefore it is possible that the transaction has completed but the field is not null. As a result we need to check /// the actual transaction status. However it can happen that the transaction has already been disposed and trying to get /// transaction status will cause ObjectDisposedException. This would also mean that the transaction has completed and can be reset. /// internal bool EnlistedInUserTransaction { get { try { return _enlistedTransaction != null && _enlistedTransaction.TransactionInformation.Status == TransactionStatus.Active; } catch (ObjectDisposedException) { _enlistedTransaction = null; return false; } } } /// /// Establish a connection to the data store by calling the Open method on the underlying data provider /// public override void Open() { if (this._storeConnection == null) throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_ConnectionStringNeededBeforeOperation); if (this.State != ConnectionState.Closed) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_CannotReopenConnection); } bool closeStoreConnectionOnFailure = false; OpenStoreConnectionIf(this._storeConnection.State != ConnectionState.Open, this._storeConnection, null, EntityRes.EntityClient_ProviderSpecificError, @"Open", ref closeStoreConnectionOnFailure); // the following guards against the case when the user closes the underlying store connection // in the state change event handler, as a consequence of which we are in the 'Broken' state if (this._storeConnection == null || this._storeConnection.State != ConnectionState.Open) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_ConnectionNotOpen); } InitializeMetadata(this._storeConnection, this._storeConnection, closeStoreConnectionOnFailure); SetEntityClientConnectionStateToOpen(); } /// /// Helper method that conditionally opens a specified store connection /// /// The condition to evaluate /// The store connection to open /// The original store connection associated with the entity client /// A flag that is set on if the connection is opened /// successfully private void OpenStoreConnectionIf(bool openCondition, DbConnection storeConnectionToOpen, DbConnection originalConnection, string exceptionCode, string attemptedOperation, ref bool closeStoreConnectionOnFailure) { try { if (openCondition) { storeConnectionToOpen.Open(); closeStoreConnectionOnFailure = true; } ResetStoreConnection(storeConnectionToOpen, originalConnection, false); // With every successful open of the store connection, always null out the current db transaction and enlistedTransaction ClearTransactions(); } catch (Exception e) { if (EntityUtil.IsCatchableExceptionType(e)) { string exceptionMessage = string.IsNullOrEmpty(attemptedOperation) ? EntityRes.GetString(exceptionCode) : EntityRes.GetString(exceptionCode, attemptedOperation); throw EntityUtil.ProviderExceptionWithMessage(exceptionMessage, e); } throw; } } /// /// Helper method to initialize the metadata workspace and reset the store connection /// associated with the entity client /// /// The new connection to associate with the entity client /// The original connection associated with the entity client /// A flag to indicate whether the original /// store connection needs to be closed on failure private void InitializeMetadata(DbConnection newConnection, DbConnection originalConnection, bool closeOriginalConnectionOnFailure) { try { // Ensure metadata is loaded and the workspace is appropriately initialized. GetMetadataWorkspace(); } catch (Exception ex) { // Undo the open if something failed if (EntityUtil.IsCatchableExceptionType(ex)) { ResetStoreConnection(newConnection, originalConnection, closeOriginalConnectionOnFailure); } throw; } } /// /// Set the entity client connection state to Open, and raise an appropriate event /// private void SetEntityClientConnectionStateToOpen() { this._entityClientConnectionState = ConnectionState.Open; OnStateChange(StateChangeOpen); } /// /// This method sets the store connection and hooks up the event /// /// The DbConnection to set /// The original DbConnection to be closed - this argument could be null /// Indicates whether the original store connection should be closed private void ResetStoreConnection(DbConnection newConnection, DbConnection originalConnection, bool closeOriginalConnection) { this._storeConnection = newConnection; if (closeOriginalConnection && (originalConnection != null)) { originalConnection.Close(); } } /// /// Create a new command object that uses this connection object. /// public new EntityCommand CreateCommand() { return new EntityCommand(null, this); } /// /// Create a new command object that uses this connection object /// protected override DbCommand CreateDbCommand() { return this.CreateCommand(); } /// /// Close the connection to the data store /// public override void Close() { // It's a no-op if there isn't an underlying connection if (this._storeConnection == null) return; this.CloseHelper(); } /// /// Changes the current database for this connection /// /// The name of the database to change to public override void ChangeDatabase(string databaseName) { throw EntityUtil.NotSupported(); } /// /// Begins a database transaction /// /// An object representing the new transaction public new EntityTransaction BeginTransaction() { return base.BeginTransaction() as EntityTransaction; } /// /// Begins a database transaction /// /// The isolation level of the transaction /// An object representing the new transaction public new EntityTransaction BeginTransaction(IsolationLevel isolationLevel) { return base.BeginTransaction(isolationLevel) as EntityTransaction; } /// /// Begins a database transaction /// /// The isolation level of the transaction /// An object representing the new transaction protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) { if (CurrentTransaction != null) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_TransactionAlreadyStarted); } if (this._storeConnection == null) throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_ConnectionStringNeededBeforeOperation); if (this.State != ConnectionState.Open) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_ConnectionNotOpen); } DbTransaction storeTransaction = null; try { storeTransaction = this._storeConnection.BeginTransaction(isolationLevel); } catch (Exception e) { if (EntityUtil.IsCatchableExceptionType(e)) { throw EntityUtil.ProviderExceptionWithMessage( System.Data.Entity.Strings.EntityClient_ErrorInBeginningTransaction, e ); } throw; } // The provider is problematic if it succeeded in beginning a transaction but returned a null // for the transaction object if (storeTransaction == null) { throw EntityUtil.ProviderIncompatible(System.Data.Entity.Strings.EntityClient_ReturnedNullOnProviderMethod("BeginTransaction", _storeConnection.GetType().Name)); } _currentTransaction = new EntityTransaction(this, storeTransaction); return _currentTransaction; } /// /// Enlist in the given transaction /// /// The transaction object to enlist into public override void EnlistTransaction(Transaction transaction) { if (_storeConnection == null) throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_ConnectionStringNeededBeforeOperation); if (this.State != ConnectionState.Open) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_ConnectionNotOpen); } try { _storeConnection.EnlistTransaction(transaction); // null means "Unenlist transaction". It is fine if no transaction is in progress (no op). Otherwise // _storeConnection.EnlistTransaction should throw and we would not get here. Debug.Assert(transaction != null || !EnlistedInUserTransaction, "DbConnection should not allow unenlist from a transaction that has not completed."); // It is OK to enlist in null transaction or multiple times in the same transaction. // In the latter case we don't need to be called multiple times when the transaction completes // so subscribe only when enlisting for the first time. Note that _storeConnection.EnlistTransaction // will throw in invalid cases (like enlisting the connection in a transaction when another // transaction has not completed) so when we get here we are sure that either no transactions are // active or the transaction the caller tries enlisting to // is the active transaction. if (transaction != null && !EnlistedInUserTransaction) { transaction.TransactionCompleted += EnlistedTransactionCompleted; } _enlistedTransaction = transaction; } catch (Exception e) { if (EntityUtil.IsCatchableExceptionType(e)) { throw EntityUtil.Provider(@"EnlistTransaction", e); } throw; } } /// /// Cleans up this connection object /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_currentTransaction")] [ResourceExposure(ResourceScope.None)] //We are not exposing any resource [ResourceConsumption(ResourceScope.Machine, ResourceScope.Machine)] //For ChangeConnectionString method call. But since the connection string we pass in is an Empty String, //we consume the resource and do not expose it any further. protected override void Dispose(bool disposing) { // It is possible for the EntityConnection to be finalized even if the object was not actually // created due to a "won't fix" bug in the x86 JITer--see Dev10 bug 892884. // Even without this bug, a stack overflow trying to allocate space to run the constructor can // result in effectively the same situation. This means we can end up finalizing objects that // have not even been fully initialized. In order for this to work we have to be very careful // what we do in Dispose and we need to stick rigidly to the "only dispose unmanaged resources // if disposing is false" rule. We don't actually have any unmanaged resources--these are // handled by the base class or other managed classes that we have references to. These classes // will dispose of their unmanaged resources on finalize, so we shouldn't try to do it here. if (disposing) { ClearTransactions(); bool raiseStateChangeEvent = EntityCloseHelper(false, this.State); if (this._storeConnection != null) { StoreCloseHelper(); // closes store connection if (this._storeConnection != null) { if (!this._userOwnsStoreConnection) // only dispose it if we didn't get it from the user... { this._storeConnection.Dispose(); } this._storeConnection = null; } } // Change the connection string to just an empty string, ChangeConnectionString should always succeed here, // it's unnecessary to pass in the connection string parameter name in the second argument, which we don't // have anyway this.ChangeConnectionString(String.Empty); if (raiseStateChangeEvent) // we need to raise the event explicitly { OnStateChange(StateChangeClosed); } } base.Dispose(disposing); } /// /// Reinitialize this connection object to use the new connection string /// /// The new connection string [ResourceExposure(ResourceScope.Machine)] //Exposes the file names which are a Machine resource as part of the connection string private void ChangeConnectionString(string newConnectionString) { DbConnectionOptions userConnectionOptions = s_emptyConnectionOptions; if (!String.IsNullOrEmpty(newConnectionString)) { userConnectionOptions = new DbConnectionOptions(newConnectionString, EntityConnectionStringBuilder.Synonyms); } DbProviderFactory factory = null; DbConnection storeConnection = null; DbConnectionOptions effectiveConnectionOptions = userConnectionOptions; if (!userConnectionOptions.IsEmpty) { // Check if we have the named connection, if yes, then use the connection string from the configuration manager settings string namedConnection = userConnectionOptions[EntityConnectionStringBuilder.NameParameterName]; if (!string.IsNullOrEmpty(namedConnection)) { // There cannot be other parameters when the named connection is specified if (1 < userConnectionOptions.Parsetable.Count) { throw EntityUtil.Argument(System.Data.Entity.Strings.EntityClient_ExtraParametersWithNamedConnection); } // Find the named connection from the configuration, then extract the settings ConnectionStringSettings setting = ConfigurationManager.ConnectionStrings[namedConnection]; if (setting == null || setting.ProviderName != EntityConnection.s_entityClientProviderName) { throw EntityUtil.Argument(System.Data.Entity.Strings.EntityClient_InvalidNamedConnection); } effectiveConnectionOptions = new DbConnectionOptions(setting.ConnectionString, EntityConnectionStringBuilder.Synonyms); // Check for a nested Name keyword string nestedNamedConnection = effectiveConnectionOptions[EntityConnectionStringBuilder.NameParameterName]; if (!string.IsNullOrEmpty(nestedNamedConnection)) { throw EntityUtil.Argument(System.Data.Entity.Strings.EntityClient_NestedNamedConnection(namedConnection)); } } //Validate the connection string has the required Keywords( Provider and Metadata) //We trim the values for both the Keywords, so a string value with only spaces will throw an exception //reporting back to the user that the Keyword was missing. ValidateValueForTheKeyword(effectiveConnectionOptions, EntityConnectionStringBuilder.MetadataParameterName); string providerName = ValidateValueForTheKeyword(effectiveConnectionOptions, EntityConnectionStringBuilder.ProviderParameterName); // Get the correct provider factory factory = GetFactory(providerName); // Create the underlying provider specific connection and give it the connection string from the DbConnectionOptions object storeConnection = GetStoreConnection(factory); try { // When the value of 'Provider Connection String' is null, it means it has not been present in the entity connection string at all. // Providers should still be able handle empty connection strings since those may be explicitly passed by clients. string providerConnectionString = effectiveConnectionOptions[EntityConnectionStringBuilder.ProviderConnectionStringParameterName]; if (providerConnectionString != null) { storeConnection.ConnectionString = providerConnectionString; } } catch (Exception e) { if (EntityUtil.IsCatchableExceptionType(e)) { throw EntityUtil.Provider(@"ConnectionString", e); } throw; } } // This lock is to ensure that the connection string matches with the provider connection and metadata workspace that's being // managed by this EntityConnection, so states in this connection object are not messed up. // It's not for security, but just to help reduce user error. lock (_connectionStringLock) { // Now we have sufficient information and verified the configuration string is good, use them for this connection object // Failure should not occur from this point to the end of this method this._providerFactory = factory; this._metadataWorkspace = null; ClearTransactions(); ResetStoreConnection(storeConnection, null, false); // Remembers the connection options objects with the connection string set by the user this._userConnectionOptions = userConnectionOptions; this._effectiveConnectionOptions = effectiveConnectionOptions; } } private static string ValidateValueForTheKeyword(DbConnectionOptions effectiveConnectionOptions, string keywordName) { string keywordValue = effectiveConnectionOptions[keywordName]; if (!string.IsNullOrEmpty(keywordValue)) keywordValue = keywordValue.Trim(); // be nice to user, always trim the value // Check that we have a non-null and non-empty value for the keyword if (string.IsNullOrEmpty(keywordValue)) { throw EntityUtil.Argument(System.Data.Entity.Strings.EntityClient_ConnectionStringMissingInfo(keywordName)); } return keywordValue; } private static EdmItemCollection LoadEdmItemCollection(MetadataWorkspace workspace, MetadataArtifactLoader artifactLoader) { // Build a string as the key and look up the MetadataCache for a match string edmCacheKey = CreateMetadataCacheKey(artifactLoader.GetOriginalPaths(DataSpace.CSpace), null, null); // Check the MetadataCache for an entry with this key object entryToken; EdmItemCollection edmItemCollection = MetadataCache.GetOrCreateEdmItemCollection(edmCacheKey, artifactLoader, out entryToken); workspace.RegisterItemCollection(edmItemCollection); // Adding the edm metadata entry token to the workspace, to make sure that this token remains alive till workspace is alive workspace.AddMetadataEntryToken(entryToken); return edmItemCollection; } private static void LoadStoreItemCollections(MetadataWorkspace workspace, DbConnection storeConnection, DbProviderFactory factory, DbConnectionOptions connectionOptions, EdmItemCollection edmItemCollection, MetadataArtifactLoader artifactLoader) { Debug.Assert(workspace.IsItemCollectionAlreadyRegistered(DataSpace.CSpace), "C-Space must be loaded before loading S or C-S space"); // The provider connection string is optional; if it has not been specified, // we pick up the store's connection string. // string providerConnectionString = connectionOptions[EntityConnectionStringBuilder.ProviderConnectionStringParameterName]; if (string.IsNullOrEmpty(providerConnectionString) && (storeConnection != null)) { providerConnectionString = storeConnection.ConnectionString; } // Build a string as the key and look up the MetadataCache for a match string storeCacheKey = CreateMetadataCacheKey(artifactLoader.GetOriginalPaths(), connectionOptions[EntityConnectionStringBuilder.ProviderParameterName], providerConnectionString); // Load store metadata. object entryToken; StorageMappingItemCollection mappingCollection = MetadataCache.GetOrCreateStoreAndMappingItemCollections(storeCacheKey, artifactLoader, edmItemCollection, out entryToken); workspace.RegisterItemCollection(mappingCollection.StoreItemCollection); workspace.RegisterItemCollection(mappingCollection); // Adding the store metadata entry token to the workspace workspace.AddMetadataEntryToken(entryToken); } private static string GetErrorMessageWorthyProviderName(DbProviderFactory factory) { EntityUtil.CheckArgumentNull(factory, "factory"); string providerName; if (!EntityUtil.TryGetProviderInvariantName(factory, out providerName)) { providerName = factory.GetType().FullName; } return providerName; } /// /// Create a key to be used with the MetadataCache from a connection options object /// /// A list of metadata file paths /// The provider name /// The provider connection string /// The key private static string CreateMetadataCacheKey(IList paths, string providerName, string providerConnectionString) { int resultCount = 0; string result; // Do a first pass to calculate the output size of the metadata cache key, // then another pass to populate a StringBuilder with the exact size and // get the result. CreateMetadataCacheKeyWithCount(paths, providerName, providerConnectionString, false, ref resultCount, out result); CreateMetadataCacheKeyWithCount(paths, providerName, providerConnectionString, true, ref resultCount, out result); return result; } /// /// Create a key to be used with the MetadataCache from a connection options /// object. /// /// A list of metadata file paths /// The provider name /// The provider connection string /// Whether the result variable should be built. /// /// On entry, the expected size of the result (unused if buildResult is false). /// After execution, the effective result. /// The key. /// /// This method should be called once with buildResult=false, to get /// the size of the resulting key, and once with buildResult=true /// and the size specification. /// private static void CreateMetadataCacheKeyWithCount(IList paths, string providerName, string providerConnectionString, bool buildResult, ref int resultCount, out string result) { // Build a string as the key and look up the MetadataCache for a match StringBuilder keyString; if (buildResult) { keyString = new StringBuilder(resultCount); } else { keyString = null; } // At this point, we've already used resultCount. Reset it // to zero to make the final debug assertion that our computation // is correct. resultCount = 0; if (!string.IsNullOrEmpty(providerName)) { resultCount += providerName.Length + 1; if (buildResult) { keyString.Append(providerName); keyString.Append(EntityConnection.s_semicolonSeparator); } } if (paths != null) { for (int i = 0; i < paths.Count; i++) { if (paths[i].Length > 0) { if (i > 0) { resultCount++; if (buildResult) { keyString.Append(EntityConnection.s_metadataPathSeparator); } } resultCount += paths[i].Length; if (buildResult) { keyString.Append(paths[i]); } } } resultCount++; if (buildResult) { keyString.Append(EntityConnection.s_semicolonSeparator); } } if (!string.IsNullOrEmpty(providerConnectionString)) { resultCount += providerConnectionString.Length; if (buildResult) { keyString.Append(providerConnectionString); } } if (buildResult) { result = keyString.ToString(); } else { result = null; } System.Diagnostics.Debug.Assert( !buildResult || (result.Length == resultCount)); } /// /// Clears the current DbTransaction and the transaction the user enlisted the connection in /// with EnlistTransaction() method. /// private void ClearTransactions() { ClearCurrentTransaction(); ClearEnlistedTransaction(); } /// /// Clears the current DbTransaction for this connection /// internal void ClearCurrentTransaction() { _currentTransaction = null; } /// /// Clears the transaction the user elinsted in using EnlistTransaction() method. /// private void ClearEnlistedTransaction() { if (EnlistedInUserTransaction) { _enlistedTransaction.TransactionCompleted -= EnlistedTransactionCompleted; } _enlistedTransaction = null; } /// /// Event handler invoked when the transaction has completed (either by committing or rolling back). /// /// The source of the event. /// The that contains the event data. /// Note that to avoid threading issues we never reset the field here. private void EnlistedTransactionCompleted(object sender, TransactionEventArgs e) { e.Transaction.TransactionCompleted -= EnlistedTransactionCompleted; } /// /// Helper method invoked as part of Close()/Dispose() that releases the underlying /// store connection and raises the appropriate event. /// private void CloseHelper() { ConnectionState previousState = this.State; // the public connection state before cleanup StoreCloseHelper(); EntityCloseHelper( true, // raise the state change event previousState ); } /// /// Store-specific helper method invoked as part of Close()/Dispose(). /// private void StoreCloseHelper() { try { if (this._storeConnection != null && (this._storeConnection.State != ConnectionState.Closed)) { this._storeConnection.Close(); } // Need to disassociate the transaction objects with this connection ClearTransactions(); } catch (Exception e) { if (EntityUtil.IsCatchableExceptionType(e)) { throw EntityUtil.ProviderExceptionWithMessage( System.Data.Entity.Strings.EntityClient_ErrorInClosingConnection, e ); } throw; } } /// /// Entity-specific helper method invoked as part of Close()/Dispose(). /// /// Indicates whether we need to raise the state change event here /// The public state of the connection before cleanup began /// true if the caller needs to raise the state change event private bool EntityCloseHelper(bool fireEventOnStateChange, ConnectionState previousState) { bool result = false; this._entityClientConnectionState = ConnectionState.Closed; if (previousState == ConnectionState.Open) { if (fireEventOnStateChange) { OnStateChange(StateChangeClosed); } else { result = true; // we didn't raise the event here; the caller should do that } } return result; } /// /// Call to determine if changes to the entity object are currently permitted. /// private void ValidateChangesPermitted() { if (_initialized) { throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_SettingsCannotBeChangedOnOpenConnection); } } /// /// Returns the DbProviderFactory associated with specified provider string /// private DbProviderFactory GetFactory(string providerString) { try { return DbProviderFactories.GetFactory(providerString); } catch (ArgumentException e) { throw EntityUtil.Argument(System.Data.Entity.Strings.EntityClient_InvalidStoreProvider, e); } } /// /// Uses DbProviderFactory to create a DbConnection /// private DbConnection GetStoreConnection(DbProviderFactory factory) { DbConnection storeConnection = factory.CreateConnection(); if (storeConnection == null) { throw EntityUtil.ProviderIncompatible(System.Data.Entity.Strings.EntityClient_ReturnedNullOnProviderMethod("CreateConnection", factory.GetType().Name)); } return storeConnection; } } }