|
//---------------------------------------------------------------------
// <copyright file="EntityConnection.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//
// @owner Microsoft
// @backupOwner Microsoft
//---------------------------------------------------------------------
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
{
/// <summary>
/// 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.
/// </summary>
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;
/// <summary>
/// Constructs the EntityConnection object with a connection not yet associated to a particular store
/// </summary>
[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)
{
}
/// <summary>
/// Constructs the EntityConnection object with a connection string
/// </summary>
/// <param name="connectionString">The connection string, may contain a list of settings for the connection or
/// just the name of the connection to use</param>
[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);
}
/// <summary>
/// Constructs the EntityConnection from Metadata loaded in memory
/// </summary>
/// <param name="workspace">Workspace containing metadata information.</param>
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;
}
/// <summary>
/// Get or set the entity connection string associated with this connection object
/// </summary>
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);
}
}
/// <summary>
/// Formats provider string to replace " with \" so it can be appended within quotation marks "..."
/// </summary>
private static string FormatProviderString(string providerString)
{
return providerString.Trim().Replace("\"", "\\\"");
}
/// <summary>
/// Get the time to wait when attempting to establish a connection before ending the try and generating an error
/// </summary>
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;
}
}
}
/// <summary>
/// Get the name of the current database or the database that will be used after a connection is opened
/// </summary>
public override string Database
{
get
{
return String.Empty;
}
}
/// <summary>
/// Gets the ConnectionState property of the EntityConnection
/// </summary>
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;
}
}
}
/// <summary>
/// Gets the name or network address of the data source to connect to
/// </summary>
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;
}
}
}
/// <summary>
/// Gets a string that contains the version of the data store to which the client is connected
/// </summary>
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;
}
}
}
/// <summary>
/// Gets the provider factory associated with EntityConnection
/// </summary>
override protected DbProviderFactory DbProviderFactory
{
get
{
return EntityProviderFactory.Instance;
}
}
/// <summary>
/// Gets the DbProviderFactory for the underlying provider
/// </summary>
internal DbProviderFactory StoreProviderFactory
{
get
{
return this._providerFactory;
}
}
/// <summary>
/// Gets the DbConnection for the underlying provider connection
/// </summary>
public DbConnection StoreConnection
{
get
{
return this._storeConnection;
}
}
/// <summary>
/// Gets the metadata workspace used by this connection
/// </summary>
[CLSCompliant(false)]
public MetadataWorkspace GetMetadataWorkspace()
{
return GetMetadataWorkspace(true /* initializeAllCollections */);
}
private bool ShouldRecalculateMetadataArtifactLoader(List<MetadataArtifactLoader> 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<MetadataArtifactLoader> loaders = new List<MetadataArtifactLoader>();
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;
}
/// <summary>
/// Gets the current transaction that this connection is enlisted in
/// </summary>
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;
}
}
/// <summary>
/// Whether the user has enlisted in transaction using EnlistTransaction method
/// </summary>
/// <remarks>
/// To avoid threading issues the <see cref="_enlistedTransaction"/> 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.
/// </remarks>
internal bool EnlistedInUserTransaction
{
get
{
try
{
return _enlistedTransaction != null && _enlistedTransaction.TransactionInformation.Status == TransactionStatus.Active;
}
catch (ObjectDisposedException)
{
_enlistedTransaction = null;
return false;
}
}
}
/// <summary>
/// Establish a connection to the data store by calling the Open method on the underlying data provider
/// </summary>
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();
}
/// <summary>
/// Helper method that conditionally opens a specified store connection
/// </summary>
/// <param name="openCondition">The condition to evaluate</param>
/// <param name="storeConnectionToOpen">The store connection to open</param>
/// <param name="originalConnection">The original store connection associated with the entity client</param>
/// <param name="closeStoreConnectionOnFailure">A flag that is set on if the connection is opened
/// successfully</param>
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;
}
}
/// <summary>
/// Helper method to initialize the metadata workspace and reset the store connection
/// associated with the entity client
/// </summary>
/// <param name="newConnection">The new connection to associate with the entity client</param>
/// <param name="originalConnection">The original connection associated with the entity client</param>
/// <param name="closeOriginalConnectionOnFailure">A flag to indicate whether the original
/// store connection needs to be closed on failure</param>
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;
}
}
/// <summary>
/// Set the entity client connection state to Open, and raise an appropriate event
/// </summary>
private void SetEntityClientConnectionStateToOpen()
{
this._entityClientConnectionState = ConnectionState.Open;
OnStateChange(StateChangeOpen);
}
/// <summary>
/// This method sets the store connection and hooks up the event
/// </summary>
/// <param name="newConnection">The DbConnection to set</param>
/// <param name="originalConnection">The original DbConnection to be closed - this argument could be null</param>
/// <param name="closeOriginalConnection">Indicates whether the original store connection should be closed</param>
private void ResetStoreConnection(DbConnection newConnection, DbConnection originalConnection, bool closeOriginalConnection)
{
this._storeConnection = newConnection;
if (closeOriginalConnection && (originalConnection != null))
{
originalConnection.Close();
}
}
/// <summary>
/// Create a new command object that uses this connection object.
/// </summary>
public new EntityCommand CreateCommand()
{
return new EntityCommand(null, this);
}
/// <summary>
/// Create a new command object that uses this connection object
/// </summary>
protected override DbCommand CreateDbCommand()
{
return this.CreateCommand();
}
/// <summary>
/// Close the connection to the data store
/// </summary>
public override void Close()
{
// It's a no-op if there isn't an underlying connection
if (this._storeConnection == null)
return;
this.CloseHelper();
}
/// <summary>
/// Changes the current database for this connection
/// </summary>
/// <param name="databaseName">The name of the database to change to</param>
public override void ChangeDatabase(string databaseName)
{
throw EntityUtil.NotSupported();
}
/// <summary>
/// Begins a database transaction
/// </summary>
/// <returns>An object representing the new transaction</returns>
public new EntityTransaction BeginTransaction()
{
return base.BeginTransaction() as EntityTransaction;
}
/// <summary>
/// Begins a database transaction
/// </summary>
/// <param name="isolationLevel">The isolation level of the transaction</param>
/// <returns>An object representing the new transaction</returns>
public new EntityTransaction BeginTransaction(IsolationLevel isolationLevel)
{
return base.BeginTransaction(isolationLevel) as EntityTransaction;
}
/// <summary>
/// Begins a database transaction
/// </summary>
/// <param name="isolationLevel">The isolation level of the transaction</param>
/// <returns>An object representing the new transaction</returns>
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;
}
/// <summary>
/// Enlist in the given transaction
/// </summary>
/// <param name="transaction">The transaction object to enlist into</param>
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;
}
}
/// <summary>
/// Cleans up this connection object
/// </summary>
/// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources</param>
[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);
}
/// <summary>
/// Reinitialize this connection object to use the new connection string
/// </summary>
/// <param name="newConnectionString">The new connection string</param>
[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;
}
/// <summary>
/// Create a key to be used with the MetadataCache from a connection options object
/// </summary>
/// <param name="paths">A list of metadata file paths</param>
/// <param name="providerName">The provider name</param>
/// <param name="providerConnectionString">The provider connection string</param>
/// <returns>The key</returns>
private static string CreateMetadataCacheKey(IList<string> 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;
}
/// <summary>
/// Create a key to be used with the MetadataCache from a connection options
/// object.
/// </summary>
/// <param name="paths">A list of metadata file paths</param>
/// <param name="providerName">The provider name</param>
/// <param name="providerConnectionString">The provider connection string</param>
/// <param name="buildResult">Whether the result variable should be built.</param>
/// <param name="resultCount">
/// On entry, the expected size of the result (unused if buildResult is false).
/// After execution, the effective result.</param>
/// <param name="result">The key.</param>
/// <remarks>
/// 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.
/// </remarks>
private static void CreateMetadataCacheKeyWithCount(IList<string> 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));
}
/// <summary>
/// Clears the current DbTransaction and the transaction the user enlisted the connection in
/// with EnlistTransaction() method.
/// </summary>
private void ClearTransactions()
{
ClearCurrentTransaction();
ClearEnlistedTransaction();
}
/// <summary>
/// Clears the current DbTransaction for this connection
/// </summary>
internal void ClearCurrentTransaction()
{
_currentTransaction = null;
}
/// <summary>
/// Clears the transaction the user elinsted in using EnlistTransaction() method.
/// </summary>
private void ClearEnlistedTransaction()
{
if (EnlistedInUserTransaction)
{
_enlistedTransaction.TransactionCompleted -= EnlistedTransactionCompleted;
}
_enlistedTransaction = null;
}
/// <summary>
/// Event handler invoked when the transaction has completed (either by committing or rolling back).
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="TransactionEventArgs"/> that contains the event data.</param>
/// <remarks>Note that to avoid threading issues we never reset the <see cref=" _enlistedTransaction"/> field here.</remarks>
private void EnlistedTransactionCompleted(object sender, TransactionEventArgs e)
{
e.Transaction.TransactionCompleted -= EnlistedTransactionCompleted;
}
/// <summary>
/// Helper method invoked as part of Close()/Dispose() that releases the underlying
/// store connection and raises the appropriate event.
/// </summary>
private void CloseHelper()
{
ConnectionState previousState = this.State; // the public connection state before cleanup
StoreCloseHelper();
EntityCloseHelper(
true, // raise the state change event
previousState
);
}
/// <summary>
/// Store-specific helper method invoked as part of Close()/Dispose().
/// </summary>
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;
}
}
/// <summary>
/// Entity-specific helper method invoked as part of Close()/Dispose().
/// </summary>
/// <param name="fireEventOnStateChange">Indicates whether we need to raise the state change event here</param>
/// <param name="previousState">The public state of the connection before cleanup began</param>
/// <returns>true if the caller needs to raise the state change event</returns>
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;
}
/// <summary>
/// Call to determine if changes to the entity object are currently permitted.
/// </summary>
private void ValidateChangesPermitted()
{
if (_initialized)
{
throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.EntityClient_SettingsCannotBeChangedOnOpenConnection);
}
}
/// <summary>
/// Returns the DbProviderFactory associated with specified provider string
/// </summary>
private DbProviderFactory GetFactory(string providerString)
{
try
{
return DbProviderFactories.GetFactory(providerString);
}
catch (ArgumentException e)
{
throw EntityUtil.Argument(System.Data.Entity.Strings.EntityClient_InvalidStoreProvider, e);
}
}
/// <summary>
/// Uses DbProviderFactory to create a DbConnection
/// </summary>
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;
}
}
}
|