//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // // // @owner [....] // @backupOwner [....] //------------------------------------------------------------------------------ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Configuration; using System.ComponentModel.Design; using System.Data; using System.Data.Common; using System.Data.EntityClient; using System.Data.Mapping; using System.Data.Metadata.Edm; using System.Web.UI.Design.WebControls.Util; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Web.Configuration; using System.Web.UI; using System.Web.UI.Design; using System.Web.UI.WebControls; using System.Windows.Forms; using System.Xml; namespace System.Web.UI.Design.WebControls { internal class EntityDataSourceDesignerHelper { #region Private and Internal static constants private static readonly string s_virtualRoot = "~/"; private static readonly string s_ecmPublicKeyToken = "PublicKeyToken=" + AssemblyRef.EcmaPublicKey; private static readonly string s_entityClientProviderName = "System.Data.EntityClient"; private static readonly string s_metadataPathSeparator = "|"; private static readonly string s_resPathPrefix = "res://"; private static readonly string s_relativeParentFolder = "../"; private static readonly string s_relativeCurrentFolder = "./"; private static readonly string s_altRelativeParentFolder = @"..\"; private static readonly string s_altRelativeCurrentFolder = @".\"; private static readonly string s_dataDirectoryNoPipes = "DataDirectory"; private static readonly string s_dataDirectory = "|DataDirectory|"; private static readonly string s_dataDirectoryPath = String.Concat(s_virtualRoot, "app_data"); private static readonly string s_resolvedResPathFormat = String.Concat(s_resPathPrefix, "{0}/{1}"); private static readonly string DesignerStateDataSourceSchemaKey = "EntityDataSourceSchema"; private static readonly string DesignerStateDataSourceConnectionStringKey = "EntityDataSourceConnectionString"; private static readonly string DesignerStateDataSourceDefaultContainerNameKey = "EntityDataSourceDefaultContainerName"; private static readonly string DesignerStateDataSourceEntitySetNameKey = "EntityDataSourceEntitySetNameKey"; private static readonly string DesignerStateDataSourceSelectKey = "EntityDataSourceSelectKey"; private static readonly string DesignerStateDataSourceCommandTextKey = "EntityDataSourceCommandTextKey"; private static readonly string DesignerStateDataSourceEnableFlatteningKey = "EntityDataSourceEnableFlattening"; internal static readonly string DefaultViewName = "DefaultView"; #endregion #region Private instance fields private readonly EntityDataSource _entityDataSource; private EntityConnection _entityConnection; private readonly IWebApplication _webApplication; // determines if any errors or warnings are displayed and if the EntityConnection and metadata are automatically loaded when accessed private bool _interactiveMode; private HashSet _assemblies; private EntityDesignerDataSourceView _view; private bool _forceSchemaRetrieval; private readonly EntityDataSourceDesigner _owner; private bool _canLoadWebConfig; private bool _usingEntityFrameworkVersionHigherThanFive = false; #endregion internal EntityDataSourceDesignerHelper(EntityDataSource entityDataSource, bool interactiveMode) { Debug.Assert(entityDataSource != null, "null entityDataSource"); _entityDataSource = entityDataSource; _interactiveMode = interactiveMode; _canLoadWebConfig = true; IServiceProvider serviceProvider = _entityDataSource.Site; if (serviceProvider != null) { // Get the designer instance associated with the specified data source control IDesignerHost designerHost = (IDesignerHost)serviceProvider.GetService(typeof(IDesignerHost)); if (designerHost != null) { _owner = designerHost.GetDesigner(this.EntityDataSource) as EntityDataSourceDesigner; } // Get other services used to determine design-time information _webApplication = serviceProvider.GetService(typeof(IWebApplication)) as IWebApplication; } Debug.Assert(_owner != null, "expected non-null owner"); Debug.Assert(_webApplication != null, "expected non-null web application service"); } internal void AddSystemWebEntityReference() { IServiceProvider serviceProvider = _entityDataSource.Site; if (serviceProvider != null) { ITypeResolutionService typeResProvider = (ITypeResolutionService)serviceProvider.GetService(typeof(ITypeResolutionService)); if (typeResProvider != null) { try { // Adding the reference using just the name and public key since we don't want to be // tied to a particular version here. typeResProvider.ReferenceAssembly( new AssemblyName("System.Web.Entity,PublicKeyToken=" + AssemblyRef.EcmaPublicKey)); } catch (FileNotFoundException) { Debug.Fail("Failed to find System.Web.Entity assembly."); // Intentionally ignored exception - the assembly should always be // found, but if it isn't, then we don't want to stop the rest of the // control from working, especially since the assembly may not always // be required. } } } } #region Helpers for EntityDataSource properties internal bool AutoGenerateWhereClause { get { return _entityDataSource.AutoGenerateWhereClause; } } internal bool AutoGenerateOrderByClause { get { return _entityDataSource.AutoGenerateOrderByClause; } } internal bool CanPage { get { DataSourceView view = ((IDataSource)_entityDataSource).GetView(DefaultViewName); if (view != null) { return view.CanPage; } return false; } } internal bool CanSort { get { DataSourceView view = ((IDataSource)_entityDataSource).GetView(DefaultViewName); if (view != null) { return view.CanSort; } return false; } } internal string ConnectionString { get { return _entityDataSource.ConnectionString; } set { if (value != ConnectionString) { _entityDataSource.ConnectionString = value; _owner.FireOnDataSourceChanged(EventArgs.Empty); } } } internal string CommandText { get { return _entityDataSource.CommandText; } set { if (value != CommandText) { _entityDataSource.CommandText = value; _owner.FireOnDataSourceChanged(EventArgs.Empty); } } } internal ParameterCollection CommandParameters { get { return _entityDataSource.CommandParameters; } } internal string DefaultContainerName { get { return _entityDataSource.DefaultContainerName; } set { if (value != DefaultContainerName) { _entityDataSource.DefaultContainerName = value; _owner.FireOnDataSourceChanged(EventArgs.Empty); } } } internal bool EnableDelete { get { return _entityDataSource.EnableDelete; } } internal bool EnableInsert { get { return _entityDataSource.EnableInsert; } } internal bool EnableUpdate { get { return _entityDataSource.EnableUpdate; } } internal bool EnableFlattening { get { return _entityDataSource.EnableFlattening; } } internal string EntitySetName { get { return _entityDataSource.EntitySetName; } set { if (value != EntitySetName) { _entityDataSource.EntitySetName = value; _owner.FireOnDataSourceChanged(EventArgs.Empty); } } } internal string EntityTypeFilter { get { return _entityDataSource.EntityTypeFilter; } set { if (value != EntityTypeFilter) { _entityDataSource.EntityTypeFilter = value; _owner.FireOnDataSourceChanged(EventArgs.Empty); } } } internal string GroupBy { get { return _entityDataSource.GroupBy; } } internal string OrderBy { get { return _entityDataSource.OrderBy; } set { if (value != OrderBy) { _entityDataSource.OrderBy = value; _owner.FireOnDataSourceChanged(EventArgs.Empty); } } } internal ParameterCollection OrderByParameters { get { return _entityDataSource.OrderByParameters; } } internal string Select { get { return _entityDataSource.Select; } set { if (value != Select) { _entityDataSource.Select = value; _owner.FireOnDataSourceChanged(EventArgs.Empty); } } } internal ParameterCollection SelectParameters { get { return _entityDataSource.SelectParameters; } } internal string Where { get { return _entityDataSource.Where; } set { if (value != Where) { _entityDataSource.Where = value; _owner.FireOnDataSourceChanged(EventArgs.Empty); } } } internal ParameterCollection WhereParameters { get { return _entityDataSource.WhereParameters; } } #endregion private EntityDataSource EntityDataSource { get { return _entityDataSource; } } private EdmItemCollection EdmItemCollection { get { // In interactive mode, we will explicitly load metadata when needed, so never load it here // When not in interactive mode, we only need to load metadata once, which is determined by the presence of an EntityConnection if (!_interactiveMode && _entityConnection == null) { LoadMetadata(); } // _entityConnection may still be null if the load failed or if we are in interactive mode and the metadata has not been explicitly loaded if (_entityConnection != null) { ItemCollection itemCollection = null; try { _entityConnection.GetMetadataWorkspace().TryGetItemCollection(DataSpace.CSpace, out itemCollection); } catch (Exception) { // Never expecting a failure because we have already initialized the workspace when the metadata was loaded, // and any errors would have been trapped then. Just ignore any errors that might occur here to prevent a crash. } return itemCollection as EdmItemCollection; // not guaranteed not to be null, caller must check anyway before using } return null; } } // The default DesignerDataSourceView private EntityDesignerDataSourceView View { get { return _view; } set { _view = value; } } /// /// The status of loading the web.config file for named connections. /// This helps to determine if we've already tried to load the web.config file and if it failed /// we do not continue to load it again. /// internal bool CanLoadWebConfig { get { return _canLoadWebConfig; } set { _canLoadWebConfig = value; } } // Whether or not a schema retrieval is being forced (used in RefreshSchema) private bool ForceSchemaRetrieval { get { return _forceSchemaRetrieval; } set { _forceSchemaRetrieval = value; } } // Loads metadata for the current connection string on the data source control private bool LoadMetadata() { EntityConnectionStringBuilder connStrBuilder = VerifyConnectionString(this.EntityDataSource.ConnectionString, true /*allowNamedConnections*/); if (connStrBuilder != null) { return LoadMetadata(connStrBuilder); } // else the connection string could not be verified, and any errors are already displayed during the failed verification, so nothing more to do return false; } internal bool LoadMetadata(EntityConnectionStringBuilder connStrBuilder) { Debug.Assert(connStrBuilder != null, "expected non-null connStrBuilder"); // if these services are not available for some reason, we will not be able to do anything useful, so don't try to load metadata if (_webApplication != null) { // _assemblies could already be loaded if this call is coming from the wizard, because metadata could be loaded // multiple times if the connection string is changed, so don't load it again if we already have it. It can't have // changed since the last load, because the wizard dialog is modal and there is no way to have changed the project between loads. if (_assemblies == null) { LoadAssemblies(); } return LoadMetadataFromBuilder(connStrBuilder); } return false; } // Loads C-Space metadata from the specified connection string builder. // Expects that the specified builder has already been verified and has minimum required properties private bool LoadMetadataFromBuilder(EntityConnectionStringBuilder connStrBuilder) { Debug.Assert(connStrBuilder != null, "expected non-null connStrBuilder"); // We will be replacing the metadata with a new collection, and if something fails to load we want to make sure to clear out any existing data ClearMetadata(); if (String.IsNullOrEmpty(connStrBuilder.ConnectionString)) { // Although we can't load metadata here, this is not an error because the user should not expect an empty connection string // to produce any metadata, so it will not be confusing when no values are available in the dropdowns. This is different from // an invalid connection string because in that case they have entered a value and are expecting to get metadata from it. return false; } // If this is a named connection, load the contents of the named connection from the web.config and verify itet if (!String.IsNullOrEmpty(connStrBuilder.Name)) { connStrBuilder = GetBuilderForNamedConnection(connStrBuilder); if (connStrBuilder == null) { // some verification failed when getting the connection string builder, // so we have nothing to load metadata from, just return return false; } } string originalMetadata = connStrBuilder.Metadata; Debug.Assert(!String.IsNullOrEmpty(originalMetadata), "originalMetadata should have aleady been verified to be non-null or empty"); List metadataWarnings = new List(); // keeps track of all warnings that happen during the parsing of the connection string List metadataPaths = new List(); // collection of resolved paths // We need to use the | separator to split the metadata value into individual paths, so first remove and process any paths // containing the macro |DataDirectory|. These will get combined again with the rest of the paths once they have been processed. List dataDirectoryPaths = new List(); string metadataWithoutDataDirectory = ResolveDataDirectory(originalMetadata, dataDirectoryPaths, metadataWarnings); foreach (string path in metadataWithoutDataDirectory.Split(new string[] { s_metadataPathSeparator }, StringSplitOptions.RemoveEmptyEntries)) { string trimmedPath = path.Trim(); if (!String.IsNullOrEmpty(trimmedPath)) { if (trimmedPath.StartsWith(s_virtualRoot, StringComparison.OrdinalIgnoreCase)) { // ~/ ResolveVirtualRootPath(trimmedPath, metadataPaths, metadataWarnings); } else if (trimmedPath.StartsWith(s_relativeCurrentFolder, StringComparison.OrdinalIgnoreCase) || trimmedPath.StartsWith(s_altRelativeCurrentFolder, StringComparison.OrdinalIgnoreCase) || trimmedPath.StartsWith(s_relativeParentFolder, StringComparison.OrdinalIgnoreCase) || trimmedPath.StartsWith(s_altRelativeParentFolder, StringComparison.OrdinalIgnoreCase)) { // ../, ..\, ./, or ..\ ResolveRelativePath(trimmedPath, metadataPaths, metadataWarnings); } else { // We are not trying to resolve any other types of paths, so just pass it along directly. // If the format of the path is unrecognized, or if the path is not valid, metadata will throw an exception that will // be displayed to the user at that time. metadataPaths.Add(trimmedPath); } } } // Add the paths with |DataDirectory| back to the list if (dataDirectoryPaths.Count > 0) { metadataPaths.AddRange(dataDirectoryPaths); } if (metadataWarnings.Count > 0) { ShowWarning(BuildWarningMessage(Strings.Warning_ConnectionStringMessageHeader, metadataWarnings)); } return SetEntityConnection(metadataPaths, connStrBuilder); } private bool SetEntityConnection(List metadataPaths, EntityConnectionStringBuilder connStrBuilder) { // It's possible the metadata was specified in the original connection string, but we filtered out everything due to not being able to resolve it to anything. // In that case, warnings have already been displayed to indicate which paths were removed, so no need to display another message. if (metadataPaths.Count > 0) { try { // Get the connection first, because it might be needed to gather provider services information DbConnection dbConnection = GetDbConnection(connStrBuilder); MetadataWorkspace metadataWorkspace = new MetadataWorkspace(metadataPaths, _assemblies); // Ensure that we have all of the item collections registered. If some of them are missing this will cause problems eventually if we need to // execute a query to get detailed schema information, but that will be handled later. For now just register everything to prevent errors in the // stack that would not be understood by the user in the designer at this point. ItemCollection edmItemCollection; ItemCollection storeItemCollection; ItemCollection csItemCollection; if (!metadataWorkspace.TryGetItemCollection(DataSpace.CSpace, out edmItemCollection)) { edmItemCollection = new EdmItemCollection(); metadataWorkspace.RegisterItemCollection(edmItemCollection); } if (!metadataWorkspace.TryGetItemCollection(DataSpace.SSpace, out storeItemCollection)) { return false; } if (!metadataWorkspace.TryGetItemCollection(DataSpace.CSSpace, out csItemCollection)) { Debug.Assert(edmItemCollection != null && storeItemCollection != null, "edm and store ItemCollection should be populated already"); metadataWorkspace.RegisterItemCollection(new StorageMappingItemCollection(edmItemCollection as EdmItemCollection, storeItemCollection as StoreItemCollection)); } // Create an ObjectItemCollection beforehand so that we can load objects by-convention metadataWorkspace.RegisterItemCollection(new ObjectItemCollection()); // Load OSpace metadata from all of the assemblies we know about foreach (Assembly assembly in _assemblies) { metadataWorkspace.LoadFromAssembly(assembly); } if (dbConnection != null) { _entityConnection = new EntityConnection(metadataWorkspace, dbConnection); return true; } // else the DbConnection could not be created and the error should have already been displayed } catch (Exception ex) { StringBuilder exceptionMessage = new StringBuilder(); if (_usingEntityFrameworkVersionHigherThanFive) { exceptionMessage.Append(Strings.Error_UnsupportedVersionOfEntityFramework); } else { exceptionMessage.AppendLine(Strings.Error_MetadataLoadError); exceptionMessage.AppendLine(); exceptionMessage.Append(ex.Message); } ShowError(exceptionMessage.ToString()); } } return false; } // Clears out the existing metadata internal void ClearMetadata() { _entityConnection = null; } /// /// Finds and caches all non system assemblies that are found using the TypeDiscoveryService /// This searches places like the ~/bin folder, app_code, and the 'assemblies' section of web.config, among others /// private void LoadAssemblies() { _assemblies = new HashSet(); // Find the assemblies using the ITypeDiscoveryService ITypeDiscoveryService typeDiscoverySvc = this.EntityDataSource.Site.GetService(typeof(ITypeDiscoveryService)) as ITypeDiscoveryService; if (typeDiscoverySvc != null) { foreach (Type type in typeDiscoverySvc.GetTypes(typeof(object), false /*excludeGlobalTypes*/)) { var assembly = type.Assembly; if (!_usingEntityFrameworkVersionHigherThanFive && assembly.GetName().Name.Equals("EntityFramework", StringComparison.InvariantCultureIgnoreCase) && assembly.GetName().Version.Major > 5) { _usingEntityFrameworkVersionHigherThanFive = true; ShowError(Strings.Error_UnsupportedVersionOfEntityFramework); } if (!_assemblies.Contains(assembly) && !IsSystemAssembly(assembly.FullName)) { _assemblies.Add(assembly); } } } } // Explicitly rebuild the known assembly cache. This is done when launching the wizard and allows the wizard to pick up the latest // assemblies in the project, without having to reload them everytime the connection string changes while the wizard is running internal void ReloadResources() { Debug.Assert(_interactiveMode == true, "resource cache should only explicitly be loaded in interactive mode"); LoadAssemblies(); } // Is the assembly and its referenced assemblies not expected to have any metadata // This does not detect all possible system assemblies, but just those we can detect as system for sure private static bool IsSystemAssembly(string fullName) { return (String.Equals(fullName, "*", StringComparison.OrdinalIgnoreCase) || fullName.EndsWith(s_ecmPublicKeyToken, StringComparison.OrdinalIgnoreCase)); } // Combines all warnings into one message private string BuildWarningMessage(string headerMessage, List warnings) { Debug.Assert(warnings != null && warnings.Count > 0, "expected non-null and non-empty warnings"); StringBuilder warningMessage = new StringBuilder(); warningMessage.AppendLine(headerMessage); warningMessage.AppendLine(); foreach (string warning in warnings) { warningMessage.AppendLine(warning); } return warningMessage.ToString(); } // Get a connection string builder for the specified connection string, and do some basic verification // namedConnStrBuilder should be based on a named connection and should already have been verified to be structurally valid private EntityConnectionStringBuilder GetBuilderForNamedConnection(EntityConnectionStringBuilder namedConnStrBuilder) { Debug.Assert(namedConnStrBuilder != null && !String.IsNullOrEmpty(namedConnStrBuilder.Name), "expected non-null connStrBuilder for a named connection"); // Need to get the actual string from the web.config EntityConnectionStringBuilder connStrBuilder = null; if (CanLoadWebConfig) { try { System.Configuration.Configuration webConfig = _webApplication.OpenWebConfiguration(true /*isReadOnly*/); if (webConfig != null) { ConnectionStringSettings connStrSettings = webConfig.ConnectionStrings.ConnectionStrings[namedConnStrBuilder.Name]; if (connStrSettings != null && !String.IsNullOrEmpty(connStrSettings.ConnectionString) && connStrSettings.ProviderName == s_entityClientProviderName) { // Verify the contents of the named connection and create a new builder from it // It can't reference another named connection, and must have both the provider and metadata keywords connStrBuilder = VerifyConnectionString(connStrSettings.ConnectionString, false /*allowNamedConnections*/); } else { ShowError(Strings.Error_NamedConnectionNotFound); } } else { ShowError(Strings.Error_CannotOpenWebConfig_SpecificConnection); } } catch (ConfigurationException ce) { StringBuilder error = new StringBuilder(); error.AppendLine(Strings.Error_CannotOpenWebConfig_SpecificConnection); error.AppendLine(); error.AppendLine(ce.Message); ShowError(error.ToString()); } } // could be null if verification failed return connStrBuilder; } // Make sure we have at least some basic keywords.This method does not attempt to do as much verification as EntityClient would do. // We are just looking for a named connection, or both the provider and metadata keywords. /// /// Make sure we have at least some basic keywords.This method does not attempt to do as much verification as EntityClient would do. /// We are just looking for a named connection, or both the provider and metadata keywords. /// /// /// Connection string to be verified. Can be empty or null. /// /// Indicates if the specified string can be a named connection in the form "name=ConnectionName". /// If this method is being called with a connection string that came from a named connection entry in the web.config, this should be false /// because we do not support nested named connections. /// /// /// A new EntityConnectionStringBuilder if the basic verification succeeded, otherwise null. Can return a builder for an empty string. /// private EntityConnectionStringBuilder VerifyConnectionString(string connectionString, bool allowNamedConnections) { // Verify if we have a structurally valid connection string with both "provider" and "metadata" keywords EntityConnectionStringBuilder connStrBuilder = null; try { // Verify that it can be loaded into the builder to ensure basic valid structure and keywords connStrBuilder = new EntityConnectionStringBuilder(connectionString); } catch (ArgumentException ex) { // The message thrown from the connection string builder is not always useful to the user in this context, so add our own error text as well ShowError(Strings.Error_CreatingConnectionStringBuilder(ex.Message)); return null; } Debug.Assert(connStrBuilder != null, "expected non-null connStrBuilder"); // If the connection string is not empty, do some validation on it // (a) If this is not supposed to be a named connection, make sure it isn't // (b) Then it's not a named connection, then verify the keywords // (c) Otherwise the connection string is a named connection, no further validation is needed // devnote: Using the ConnectionString property on the builder in the check for empty, because the original connection string // could have been something like "name=", which produces an empty ConnectionString in the builder, although the original was not empty if (!String.IsNullOrEmpty(connStrBuilder.ConnectionString)) { // If named connection is not allowed, make sure it is not specified if (!allowNamedConnections && !String.IsNullOrEmpty(connStrBuilder.Name)) { ShowError(Strings.Error_NestedNamedConnection); return null; } // If the connection string is not a named connection, verify the keywords if (String.IsNullOrEmpty(connStrBuilder.Name)) { if (String.IsNullOrEmpty(connStrBuilder.Metadata)) { ShowError(Strings.Error_MissingMetadataKeyword); return null; } } // else it's a named connection and we don't need to validate it further } return connStrBuilder; } internal void ShowError(string message) { if (_interactiveMode) { UIHelper.ShowError(EntityDataSource.Site, message); } // else we are in a mode where we just want to ignore errors (typically this happens when called from the property grid) } internal void ShowWarning(string message) { if (_interactiveMode) { UIHelper.ShowWarning(EntityDataSource.Site, message); } // else we are in a mode where we just want to ignore warnings (typically this happens when called from the property grid) } // Removes any paths containing |DataDirectory| from a string of metadata locations, adds them to a separate list and expands // the macro to the full path to ~/ for any paths that start with the macro private string ResolveDataDirectory(string metadataPaths, List dataDirectoryPaths, List warnings) { Debug.Assert(dataDirectoryPaths != null, "null dataDirectoryPaths"); // 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. // int indexStart = metadataPaths.IndexOf(s_dataDirectory, StringComparison.OrdinalIgnoreCase); while (indexStart != -1) { int prevSeparatorIndex = indexStart == 0 ? -1 : metadataPaths.LastIndexOf( 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 = metadataPaths.IndexOf(s_metadataPathSeparator, indexStart + s_dataDirectory.Length, StringComparison.Ordinal); string resolvedPath; if (indexEnd == -1) { resolvedPath = ExpandDataDirectory(metadataPaths.Substring(macroPathBeginIndex), warnings); if (resolvedPath != null) { // only add to the list if no warning occurred dataDirectoryPaths.Add(resolvedPath); } metadataPaths = metadataPaths.Remove(macroPathBeginIndex); // update the concatenated list of paths break; } resolvedPath = ExpandDataDirectory(metadataPaths.Substring(macroPathBeginIndex, indexEnd - macroPathBeginIndex), warnings); if (resolvedPath != null) { // only add to the list if no warning occurred dataDirectoryPaths.Add(resolvedPath); } // Update the concatenated list of paths by removing the one containing the macro. // metadataPaths = metadataPaths.Remove(macroPathBeginIndex, indexEnd - macroPathBeginIndex); indexStart = metadataPaths.IndexOf(s_dataDirectory, StringComparison.OrdinalIgnoreCase); } return metadataPaths; } // If the specified string starts with |DataDirectory|, replace that macro with the full path for ~/app_data in the application private string ExpandDataDirectory(string pathWithMacro, List warnings) { string trimmedPath = pathWithMacro.Trim(); if (trimmedPath.StartsWith(s_dataDirectory, StringComparison.OrdinalIgnoreCase)) { string dataDirectoryPath = GetDataDirectory(); if (dataDirectoryPath != null) { return String.Concat(dataDirectoryPath, trimmedPath.Substring(s_dataDirectory.Length)); } else { warnings.Add(Strings.Warning_DataDirectoryNotFound(trimmedPath)); return null; } } // else the macro is somewhere in the middle of the string which is not valid anyway, so just pass it along and let the metadata failure occur return trimmedPath; } /// /// Resolves the |DataDirecotry| macro from the current web application /// /// The physical path for the macro expansion, or null if the data directory could not be found private string GetDataDirectory() { IProjectItem dataDirectoryPath = _webApplication.GetProjectItemFromUrl(s_dataDirectoryPath); if (dataDirectoryPath != null) { return dataDirectoryPath.PhysicalPath; } else { return null; } } private void ResolveVirtualRootPath(string resourcePath, List metadataPaths, List warnings) { IProjectItem rootItem = _webApplication.GetProjectItemFromUrl(s_virtualRoot); if (rootItem != null) { metadataPaths.Add(String.Concat(rootItem.PhysicalPath, resourcePath.Substring(s_virtualRoot.Length))); } else { warnings.Add(Strings.Warning_VirtualRootNotFound(resourcePath)); } } private void ResolveRelativePath(string resourcePath, List metadataPaths, List warnings) { IProjectItem rootItem = _webApplication.GetProjectItemFromUrl(s_virtualRoot); if (rootItem != null) { metadataPaths.Add(String.Concat(rootItem.PhysicalPath, resourcePath)); } else { warnings.Add(Strings.Warning_VirtualRootNotFound(resourcePath)); } } // Create a DbConnection for the specified connection string private DbConnection GetDbConnection(EntityConnectionStringBuilder connStrBuilder) { DbProviderFactory factory = null; if (!string.IsNullOrEmpty(connStrBuilder.Provider)) { try { // Get the correct provider factory factory = DbProviderFactories.GetFactory(connStrBuilder.Provider); } catch (Exception ex) { ShowError(Strings.Error_CannotCreateDbProviderFactory(ex.Message)); } } else { ShowError(Strings.Error_CannotCreateDbProviderFactory(Strings.Error_MissingProviderKeyword)); } if (factory != null) { try { // Create the underlying provider specific connection and give it the specified provider connection string DbConnection storeConnection = factory.CreateConnection(); if (storeConnection != null) { storeConnection.ConnectionString = connStrBuilder.ProviderConnectionString; return storeConnection; } } catch (Exception) { // eat any exceptions and just show the general error below } ShowError(Strings.Error_ReturnedNullOnProviderMethod(factory.GetType().Name)); } return null; } internal void RefreshSchema(bool preferSilent) { string originalDataDirectory = null; try { _owner.SuppressDataSourceEvents(); Cursor originalCursor = Cursor.Current; // Make sure we have set the |DataDirectory| field in the AppDomain so that the underlying providers // can make use of this macro originalDataDirectory = AppDomain.CurrentDomain.GetData(s_dataDirectoryNoPipes) as string; AppDomain.CurrentDomain.SetData(s_dataDirectoryNoPipes, GetDataDirectory()); // Verify that we can get the current schema DataTable currentSchema = GetCurrentSchema(preferSilent); if (currentSchema == null) { // error occurred when getting current schema return; } try { Cursor.Current = Cursors.WaitCursor; EntityDesignerDataSourceView view = GetView(DefaultViewName); IDataSourceViewSchema oldViewSchema = view.Schema; bool wasForceUsed = false; if (oldViewSchema == null) { ForceSchemaRetrieval = true; oldViewSchema = view.Schema; ForceSchemaRetrieval = false; wasForceUsed = true; } SaveSchema(this.ConnectionString, this.DefaultContainerName, this.EntitySetName, this.Select, this.CommandText, this.EnableFlattening, currentSchema); // Compare new schema to old schema and if it changed, raise the SchemaRefreshed event bool viewSchemaEquivalent = _owner.InternalViewSchemasEquivalent(oldViewSchema, view.Schema); if (!viewSchemaEquivalent) { _owner.FireOnSchemaRefreshed(EventArgs.Empty); } else if (wasForceUsed) { // if the schemas were equivalent but the schema retrieval was forced, still raise the data source changed event _owner.FireOnDataSourceChanged(EventArgs.Empty); } } finally { Cursor.Current = originalCursor; } } finally { // Reset the AppDomain to its original |DataDirectory| value AppDomain.CurrentDomain.SetData(s_dataDirectoryNoPipes, originalDataDirectory); _owner.ResumeDataSourceEvents(); } } private DataTable GetCurrentSchema(bool preferSilent) { // Verify that we have values for a minimum set of properties that will be required to get schema if (String.IsNullOrEmpty(this.EntityDataSource.ConnectionString) || String.IsNullOrEmpty(this.EntityDataSource.DefaultContainerName) || String.IsNullOrEmpty(this.EntityDataSource.CommandText) && String.IsNullOrEmpty(this.EntityDataSource.EntitySetName)) { if (!preferSilent) { ShowError(Strings.Error_CannotRefreshSchema_MissingProperties); } return null; } bool originalMode = _interactiveMode; try { // Suppress error messages while loading metadata if we are in silent mode _interactiveMode = !preferSilent; // In interactive mode, always clear any cached information so we are sure to get the latest schema // This is necessary in case the metadata or entity classes in referenced assemblies has changed // or in case the metadata files have changed without changing any of the properties on the control if (_interactiveMode) { ReloadResources(); ClearMetadata(); } // Try to load metadata if we don't have it yet if (_entityConnection == null) { if (!LoadMetadata()) { return null; } // else metadata was successfully loaded, so continue refreshing schema } } finally { _interactiveMode = originalMode; } // Either _entityConnection was already set, or we should have successfully loaded it Debug.Assert(_entityConnection != null, "_entityConnection should have been initialized"); try { // Create a temporary data source based on the EntityConnection we have built with // the right metadata from the design-time environment EntityDataSource entityDataSource = new EntityDataSource(_entityConnection); // This is workaround for a bug in the SQL CE provider services. SQL CE uses two providers - one is supposed to be used at design time // while the other one is supposed to be used at runtime. When the Entiy Designer is used in a way that requires to talk to the database // SQL CE starts returning design time provider. However they don't reset an internal flag and continue to return design time provider even if // the Entity Designer is not used anymore. Calling GetProviderManifestToken() method will reset the flag according to the provider in the // connection. This fixes the problem for SQL CE provider without having to special case SQL CE because it will be a no-op for other providers. // For more details see bug 35675 in DevDiv database http://vstfdevdiv:8080/web/wi.aspx?pcguid=22f9acc9-569a-41ff-b6ac-fac1b6370209&id=35675 DbProviderServices.GetProviderServices(_entityConnection.StoreConnection).GetProviderManifestToken(_entityConnection.StoreConnection); // Copy only the properties that can affect the schema entityDataSource.CommandText = this.EntityDataSource.CommandText; CopyParameters(this.EntityDataSource.CommandParameters, entityDataSource.CommandParameters); entityDataSource.DefaultContainerName = this.EntityDataSource.DefaultContainerName; entityDataSource.EntitySetName = this.EntityDataSource.EntitySetName; entityDataSource.EntityTypeFilter = this.EntityDataSource.EntityTypeFilter; entityDataSource.GroupBy = this.EntityDataSource.GroupBy; entityDataSource.Select = this.EntityDataSource.Select; entityDataSource.EnableFlattening = this.EntityDataSource.EnableFlattening; CopyParameters(this.EntityDataSource.SelectParameters, entityDataSource.SelectParameters); EntityDataSourceView view = (EntityDataSourceView)(((IDataSource)entityDataSource).GetView(DefaultViewName)); DataTable viewTable = view.GetViewSchema(); viewTable.TableName = DefaultViewName; return viewTable; } catch (Exception ex) { if (!preferSilent) { StringBuilder errorMessage = new StringBuilder(); errorMessage.AppendLine(Strings.Error_CannotRefreshSchema_RuntimeException(ex.Message)); if (ex.InnerException != null) { errorMessage.AppendLine(Strings.Error_CannotRefreshSchema_RuntimeException_InnerException(ex.InnerException.Message)); } ShowError(errorMessage.ToString()); } } return null; } private void CopyParameters(ParameterCollection originalParameters, ParameterCollection newParameters) { Debug.Assert(originalParameters != null && newParameters != null, "parameter collections on the data source should never be null"); Debug.Assert(newParameters.Count == 0, "new parameter collection should not contain any parameters yet"); _owner.CloneParameters(originalParameters, newParameters); } // Loads the schema internal DataTable LoadSchema() { if (!ForceSchemaRetrieval) { // Only check for consistency if we are not forcing the retrieval string connectionString = _owner.LoadFromDesignerState(DesignerStateDataSourceConnectionStringKey) as string; string defaultContainerName = _owner.LoadFromDesignerState(DesignerStateDataSourceDefaultContainerNameKey) as string; string entitySetName = _owner.LoadFromDesignerState(DesignerStateDataSourceEntitySetNameKey) as string; string select = _owner.LoadFromDesignerState(DesignerStateDataSourceSelectKey) as string; string commandText = _owner.LoadFromDesignerState(DesignerStateDataSourceCommandTextKey) as string; object enableFlattening = _owner.LoadFromDesignerState(DesignerStateDataSourceEnableFlatteningKey); if (!String.Equals(connectionString, this.ConnectionString, StringComparison.OrdinalIgnoreCase) || !String.Equals(defaultContainerName, this.DefaultContainerName, StringComparison.OrdinalIgnoreCase) || !String.Equals(entitySetName, this.EntitySetName, StringComparison.OrdinalIgnoreCase) || !String.Equals(select, this.Select, StringComparison.OrdinalIgnoreCase) || !String.Equals(commandText, this.CommandText, StringComparison.OrdinalIgnoreCase) || (enableFlattening == null || (((bool)enableFlattening) != this.EnableFlattening))) { return null; } } // Either we are forcing schema retrieval, or we're not forcing but we're consistent, so get the schema DataTable schema = _owner.LoadFromDesignerState(DesignerStateDataSourceSchemaKey) as DataTable; return schema; } private void SaveSchema(string connectionString, string defaultContainerName, string entitySetName, string select, string commandText, bool enableFlattening, DataTable currentSchema) { // Save the schema to DesignerState _owner.SaveDesignerState(DesignerStateDataSourceConnectionStringKey, connectionString); _owner.SaveDesignerState(DesignerStateDataSourceDefaultContainerNameKey, defaultContainerName); _owner.SaveDesignerState(DesignerStateDataSourceEntitySetNameKey, entitySetName); _owner.SaveDesignerState(DesignerStateDataSourceSelectKey, select); _owner.SaveDesignerState(DesignerStateDataSourceCommandTextKey, commandText); _owner.SaveDesignerState(DesignerStateDataSourceEnableFlatteningKey, enableFlattening); _owner.SaveDesignerState(DesignerStateDataSourceSchemaKey, currentSchema); } // Gets a view (can only get the default view) internal EntityDesignerDataSourceView GetView(string viewName) { if (String.IsNullOrEmpty(viewName) || String.Equals(viewName, DefaultViewName, StringComparison.OrdinalIgnoreCase)) { if (View == null) { View = new EntityDesignerDataSourceView(_owner); } return View; } return null; } // Gets a list of view names internal string[] GetViewNames() { return new string[] { DefaultViewName }; } // Caller can specify that the results should not be sorted if they may add something to the list and sort themselves internal List GetContainerNames(bool sortResults) { List entityContainerItems = new List(); if (this.EdmItemCollection != null) { ReadOnlyCollection entityContainers = this.EdmItemCollection.GetItems(); foreach (EntityContainer entityContainer in entityContainers) { entityContainerItems.Add(new EntityDataSourceContainerNameItem(entityContainer)); } if (sortResults) { entityContainerItems.Sort(); } } return entityContainerItems; } internal EntityDataSourceContainerNameItem GetEntityContainerItem(string entityContainerName) { if (String.IsNullOrEmpty(entityContainerName)) { return null; // can't make a valid wrapper with an empty container name } EntityContainer container = null; if (this.EdmItemCollection != null && this.EdmItemCollection.TryGetEntityContainer(entityContainerName, true /*ignoreCase*/, out container) && container != null) { return new EntityDataSourceContainerNameItem(container); } else { return new EntityDataSourceContainerNameItem(entityContainerName); } } internal List GetEntitySets(string entityContainerName) { EntityContainer container = null; if (this.EdmItemCollection != null) { this.EdmItemCollection.TryGetEntityContainer(entityContainerName, true /*ignoreCase*/, out container); } return GetEntitySets(container, true /*sortResults*/); } // Caller can specify that the results should not be sorted if they may add something to the list and sort themselves internal List GetEntitySets(EntityContainer entityContainer, bool sortResults) { List entitySetNameItems = new List(); if (entityContainer != null) { foreach (EntitySetBase entitySetBase in entityContainer.BaseEntitySets) { // BaseEntitySets returns RelationshipSets too, but we only want EntitySets if (entitySetBase.BuiltInTypeKind == BuiltInTypeKind.EntitySet) { entitySetNameItems.Add(new EntityDataSourceEntitySetNameItem(entitySetBase as EntitySet)); } } if (sortResults) { entitySetNameItems.Sort(); } } return entitySetNameItems; } // Caller can specify that the results should not be sorted if they may add something to the list and sort themselves internal List GetNamedEntityClientConnections(bool sortResults) { List namedEntityClientConnections = new List(); System.Configuration.Configuration webConfig = _webApplication.OpenWebConfiguration(true /*isReadOnly*/); if (webConfig != null) { try { foreach (ConnectionStringSettings connStrSettings in webConfig.ConnectionStrings.ConnectionStrings) { if (connStrSettings.ProviderName == s_entityClientProviderName) { EntityConnectionStringBuilder connStrBuilder = new EntityConnectionStringBuilder(); connStrBuilder.Name = connStrSettings.Name; namedEntityClientConnections.Add(new EntityConnectionStringBuilderItem(connStrBuilder)); } } if (sortResults) { namedEntityClientConnections.Sort(); } } catch (ConfigurationException ce) { CanLoadWebConfig = false; namedEntityClientConnections.Clear(); StringBuilder error = new StringBuilder(); error.AppendLine(Strings.Warning_CannotOpenWebConfig_AllConnections); error.AppendLine(); error.AppendLine(ce.Message); ShowWarning(error.ToString()); } } else { ShowWarning(Strings.Warning_CannotOpenWebConfig_AllConnections); } return namedEntityClientConnections; } internal EntityConnectionStringBuilderItem GetEntityConnectionStringBuilderItem(string connectionString) { EntityConnectionStringBuilder connStrBuilder = VerifyConnectionString(connectionString, true /*allowNamedConnections*/); if (connStrBuilder != null) { return new EntityConnectionStringBuilderItem(connStrBuilder); } else { return new EntityConnectionStringBuilderItem(connectionString); } } internal List GetEntityTypeProperties(EntityType entityType) { List properties = new List(); foreach (EdmProperty property in entityType.Properties) { properties.Add(property.Name); } Debug.Assert(properties.Count > 0, "expected entity to have at least one property"); // don't sort the properties here because it will cause them to be displayed to the user in a non-intuitive order return properties; } internal List GetEntityTypeFilters(string entityContainerName, string entitySetName) { EntityType baseEntitySetType = null; if (this.EdmItemCollection != null) { EntityContainer container; if (this.EdmItemCollection.TryGetEntityContainer(entityContainerName, true /*ignoreCase*/, out container) && (container != null)) { EntitySet entitySet; if (container.TryGetEntitySetByName(entitySetName, true /*ignoreCase*/, out entitySet) && entitySet != null) { baseEntitySetType = entitySet.ElementType; } } } return GetEntityTypeFilters(baseEntitySetType, true /*sortResults*/); } internal List GetEntityTypeFilters(EntityType baseEntitySetType, bool sortResults) { List derivedTypes = new List(); if (baseEntitySetType != null && this.EdmItemCollection != null) { foreach (EntityType entityType in GetTypeAndSubtypesOf(baseEntitySetType, this.EdmItemCollection)) { derivedTypes.Add(new EntityDataSourceEntityTypeFilterItem(entityType)); } if (sortResults) { derivedTypes.Sort(); } } return derivedTypes; } #region Helper methods for finding possible types for EntityTypeFilter private static IEnumerable GetTypeAndSubtypesOf(EntityType entityType, EdmItemCollection itemCollection) { // Always include the specified type, even if it's abstract yield return entityType; // Get the subtypes of the type from the item collection IEnumerable entityTypesInCollection = itemCollection.OfType(); foreach (EntityType typeInCollection in entityTypesInCollection) { if (entityType.Equals(typeInCollection) == false && IsStrictSubtypeOf(typeInCollection, entityType)) { yield return typeInCollection; } } yield break; } // requires: firstType is not null // effects: if otherType is among the base types, return true, // otherwise returns false. // when othertype is same as the current type, return false. private static bool IsStrictSubtypeOf(EntityType firstType, EntityType secondType) { Debug.Assert(firstType != null, "firstType should not be not null"); if (secondType == null) { return false; } // walk up my type hierarchy list for (EntityType t = (EntityType)firstType.BaseType; t != null; t = (EntityType)t.BaseType) { if (t == secondType) return true; } return false; } #endregion // Copy properties from temporary state to the data source internal void SaveEntityDataSourceProperties(EntityDataSourceState state) { this.EntityDataSource.ConnectionString = state.ConnectionString; this.EntityDataSource.DefaultContainerName = state.DefaultContainerName; this.EntityDataSource.EnableDelete = state.EnableDelete; this.EntityDataSource.EnableInsert = state.EnableInsert; this.EntityDataSource.EnableUpdate = state.EnableUpdate; this.EntityDataSource.EntitySetName = state.EntitySetName; this.EntityDataSource.EntityTypeFilter = state.EntityTypeFilter; this.EntityDataSource.Select = state.Select; this.EntityDataSource.EnableFlattening = state.EnableFlattening; } // Copy properties from the data source to temporary state internal EntityDataSourceState LoadEntityDataSourceState() { EntityDataSourceState state = new EntityDataSourceState(); state.ConnectionString = this.EntityDataSource.ConnectionString; state.DefaultContainerName = this.EntityDataSource.DefaultContainerName; state.EnableDelete = this.EntityDataSource.EnableDelete; state.EnableInsert = this.EntityDataSource.EnableInsert; state.EnableUpdate = this.EntityDataSource.EnableUpdate; state.EntitySetName = this.EntityDataSource.EntitySetName; state.EntityTypeFilter = this.EntityDataSource.EntityTypeFilter; state.Select = this.EntityDataSource.Select; return state; } } }