using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Web.DynamicData.ModelProviders; using System.Web.Resources; using System.Collections.Concurrent; namespace System.Web.DynamicData { /// /// Object that represents a database or a number of databases used by the dynamic data. It can have multiple different data contexts registered on it. /// public class MetaModel : IMetaModel { private List _contextTypes = new List(); private static object _lock = new object(); private List _tables = new List(); private ReadOnlyCollection _tablesRO; private Dictionary _tablesByUniqueName = new Dictionary(StringComparer.OrdinalIgnoreCase); private Dictionary _tablesByContextAndName = new Dictionary(); private SchemaCreator _schemaCreator; private EntityTemplateFactory _entityTemplateFactory; private IFieldTemplateFactory _fieldTemplateFactory; private FilterFactory _filterFactory; private static Exception s_registrationException; private static MetaModel s_defaultModel; private string _dynamicDataFolderVirtualPath; private HttpContextBase _context; private readonly static ConcurrentDictionary s_registeredMetadataTypes = new ConcurrentDictionary(); // Use global registration is true by default private bool _registerGlobally = true; internal virtual int RegisteredDataModelsCount { get { return _contextTypes.Count; } } /// /// ctor /// public MetaModel() : this(true /* registerGlobally */) { } public MetaModel(bool registerGlobally) : this(SchemaCreator.Instance, registerGlobally) { } // constructor for testing purposes internal MetaModel(SchemaCreator schemaCreator, bool registerGlobally) { // Create a readonly wrapper for handing out _tablesRO = new ReadOnlyCollection(_tables); _schemaCreator = schemaCreator; _registerGlobally = registerGlobally; // Don't touch Default.Model when we're not using global registration if (registerGlobally) { lock (_lock) { if (Default == null) { Default = this; } } } } internal HttpContextBase Context { get { return _context ?? HttpContext.Current.ToWrapper(); } set { _context = value; } } /// /// allows for setting of the DynamicData folder for this mode. The default is ~/DynamicData/ /// public string DynamicDataFolderVirtualPath { get { if (_dynamicDataFolderVirtualPath == null) { _dynamicDataFolderVirtualPath = "~/DynamicData/"; } return _dynamicDataFolderVirtualPath; } set { // Make sure it ends with a slash _dynamicDataFolderVirtualPath = VirtualPathUtility.AppendTrailingSlash(value); } } /// /// Returns a reference to the first instance of MetaModel that is created in an app. Provides a simple way of referencing /// the default MetaModel instance. Applications that will use multiple models will have to provide their own way of storing /// references to any additional meta models. One way of looking them up is by using the GetModel method. /// public static MetaModel Default { get { CheckForRegistrationException(); return s_defaultModel; } internal set { s_defaultModel = value; } } /// /// Gets the model instance that had the contextType registered with it /// /// A DataContext or ObjectContext type (e.g. NorthwindDataContext) /// a model public static MetaModel GetModel(Type contextType) { CheckForRegistrationException(); if (contextType == null) { throw new ArgumentNullException("contextType"); } MetaModel model; if (MetaModelManager.TryGetModel(contextType, out model)) { return model; } else { throw new InvalidOperationException(String.Format( CultureInfo.CurrentCulture, DynamicDataResources.MetaModel_ContextDoesNotBelongToModel, contextType.FullName)); } } /// /// Registers a context. Uses the default ContextConfiguration options. /// /// public void RegisterContext(Type contextType) { RegisterContext(contextType, new ContextConfiguration()); } /// /// Registers a context. Uses the the given ContextConfiguration options. /// /// /// public void RegisterContext(Type contextType, ContextConfiguration configuration) { if (contextType == null) { throw new ArgumentNullException("contextType"); } RegisterContext(() => Activator.CreateInstance(contextType), configuration); } /// /// Registers a context. Uses default ContextConfiguration. Accepts a context factory that is a delegate used for /// instantiating the context. This allows developers to instantiate context using a custom constructor. /// /// public void RegisterContext(Func contextFactory) { RegisterContext(contextFactory, new ContextConfiguration()); } /// /// Registers a context. Uses given ContextConfiguration. Accepts a context factory that is a delegate used for /// instantiating the context. This allows developers to instantiate context using a custom constructor. /// /// /// public void RegisterContext(Func contextFactory, ContextConfiguration configuration) { object contextInstance = null; try { if (contextFactory == null) { throw new ArgumentNullException("contextFactory"); } contextInstance = contextFactory(); if (contextInstance == null) { throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, DynamicDataResources.MetaModel_ContextFactoryReturnsNull), "contextFactory"); } Type contextType = contextInstance.GetType(); if (!_schemaCreator.ValidDataContextType(contextType)) { throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, DynamicDataResources.MetaModel_ContextTypeNotSupported, contextType.FullName)); } } catch (Exception e) { s_registrationException = e; throw; } // create model abstraction RegisterContext(_schemaCreator.CreateDataModel(contextInstance, contextFactory), configuration); } /// /// Register context using give model provider. Uses default context configuration. /// /// public void RegisterContext(DataModelProvider dataModelProvider) { RegisterContext(dataModelProvider, new ContextConfiguration()); } /// /// Register context using give model provider. Uses given context configuration. /// /// /// [SuppressMessage("Microsoft.Security", "CA2119:SealMethodsThatSatisfyPrivateInterfaces", Justification = "Interface is not used in any security sesitive code paths.")] public virtual void RegisterContext(DataModelProvider dataModelProvider, ContextConfiguration configuration) { if (dataModelProvider == null) { throw new ArgumentNullException("dataModelProvider"); } if (configuration == null) { throw new ArgumentNullException("configuration"); } if (_registerGlobally) { CheckForRegistrationException(); } // check if context has already been registered if (_contextTypes.Contains(dataModelProvider.ContextType)) { throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, DynamicDataResources.MetaModel_ContextAlreadyRegistered, dataModelProvider.ContextType.FullName)); } try { IEnumerable tableProviders = dataModelProvider.Tables; // create and validate model var tablesToInitialize = new List(); foreach (TableProvider tableProvider in tableProviders) { RegisterMetadataTypeDescriptionProvider(tableProvider, configuration.MetadataProviderFactory); MetaTable table = CreateTable(tableProvider); table.CreateColumns(); var tableNameAttribute = tableProvider.Attributes.OfType().SingleOrDefault(); string nameOverride = tableNameAttribute != null ? tableNameAttribute.Name : null; table.SetScaffoldAndName(configuration.ScaffoldAllTables, nameOverride); CheckTableNameConflict(table, nameOverride, tablesToInitialize); tablesToInitialize.Add(table); } _contextTypes.Add(dataModelProvider.ContextType); if (_registerGlobally) { MetaModelManager.AddModel(dataModelProvider.ContextType, this); } foreach (MetaTable table in tablesToInitialize) { AddTable(table); } // perform initialization at the very end to ensure all references will be properly registered foreach (MetaTable table in tablesToInitialize) { table.Initialize(); } } catch (Exception e) { if (_registerGlobally) { s_registrationException = e; } throw; } } internal static void CheckForRegistrationException() { if (s_registrationException != null) { throw new InvalidOperationException( String.Format(CultureInfo.CurrentCulture, DynamicDataResources.MetaModel_RegistrationErrorOccurred), s_registrationException); } } /// /// Reset any previous registration error that may have happened. Normally, the behavior is that when an error /// occurs during registration, the exception is cached and rethrown on all subsequent operations. This is done /// so that if an error occurs in Application_Start, it shows up on every request. Calling this method clears /// out the error and potentially allows new RegisterContext calls. /// public static void ResetRegistrationException() { s_registrationException = null; } // Used for unit tests internal static void ClearSimpleCache() { s_registeredMetadataTypes.Clear(); } internal static MetaModel CreateSimpleModel(Type entityType) { // Never register a TDP more than once for a type if (!s_registeredMetadataTypes.ContainsKey(entityType)) { var provider = new AssociatedMetadataTypeTypeDescriptionProvider(entityType); TypeDescriptor.AddProviderTransparent(provider, entityType); s_registeredMetadataTypes.TryAdd(entityType, true); } MetaModel model = new MetaModel(false /* registerGlobally */); // Pass a null provider factory since we registered the provider ourselves model.RegisterContext(new SimpleDataModelProvider(entityType), new ContextConfiguration { MetadataProviderFactory = null }); return model; } internal static MetaModel CreateSimpleModel(ICustomTypeDescriptor descriptor) { MetaModel model = new MetaModel(false /* registerGlobally */); // model.RegisterContext(new SimpleDataModelProvider(descriptor)); return model; } /// /// Instantiate a MetaTable object. Can be overridden to instantiate a derived type /// /// protected virtual MetaTable CreateTable(TableProvider provider) { return new MetaTable(this, provider); } private void AddTable(MetaTable table) { _tables.Add(table); _tablesByUniqueName.Add(table.Name, table); if (_registerGlobally) { MetaModelManager.AddTable(table.EntityType, table); } if (table.DataContextType != null) { // need to use the name from the provider since the name from the table could have been modified by use of TableNameAttribute _tablesByContextAndName.Add(new ContextTypeTableNamePair(table.DataContextType, table.Provider.Name), table); } } private void CheckTableNameConflict(MetaTable table, string nameOverride, List tablesToInitialize) { // try to find name conflict in tables from other context, or already processed tables in current context MetaTable nameConflictTable; if (!_tablesByUniqueName.TryGetValue(table.Name, out nameConflictTable)) { nameConflictTable = tablesToInitialize.Find(t => t.Name.Equals(table.Name, StringComparison.CurrentCulture)); } if (nameConflictTable != null) { if (String.IsNullOrEmpty(nameOverride)) { throw new ArgumentException(String.Format( CultureInfo.CurrentCulture, DynamicDataResources.MetaModel_EntityNameConflict, table.EntityType.FullName, table.DataContextType.FullName, nameConflictTable.EntityType.FullName, nameConflictTable.DataContextType.FullName)); } else { throw new ArgumentException(String.Format( CultureInfo.CurrentCulture, DynamicDataResources.MetaModel_EntityNameOverrideConflict, nameOverride, table.EntityType.FullName, table.DataContextType.FullName, nameConflictTable.EntityType.FullName, nameConflictTable.DataContextType.FullName)); } } } private static void RegisterMetadataTypeDescriptionProvider(TableProvider entity, Func providerFactory) { if (providerFactory != null) { Type entityType = entity.EntityType; // Support for type-less MetaTable if (entityType != null) { TypeDescriptionProvider provider = providerFactory(entityType); if (provider != null) { TypeDescriptor.AddProviderTransparent(provider, entityType); } } } } /// /// Returns a collection of all the tables that are part of the context, regardless of whether they are visible or not. /// public ReadOnlyCollection Tables { get { CheckForRegistrationException(); return _tablesRO; } } /// /// Returns a collection of the currently visible tables for this context. Currently visible is defined as: /// - a table whose EntityType is not abstract /// - a table with scaffolding enabled /// - a table for which a custom page for the list action can be found and that can be read by the current User /// public List VisibleTables { get { CheckForRegistrationException(); return Tables.Where(IsTableVisible).OrderBy(t => t.DisplayName).ToList(); } } private bool IsTableVisible(MetaTable table) { return !table.EntityType.IsAbstract && !String.IsNullOrEmpty(table.ListActionPath) && table.CanRead(Context.User); } /// /// Looks up a MetaTable by the entity type. Throws an exception if one is not found. /// /// /// [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "We really want this to be a Type.")] public MetaTable GetTable(Type entityType) { MetaTable table; if (!TryGetTable(entityType, out table)) { throw new ArgumentException(String.Format( CultureInfo.CurrentCulture, DynamicDataResources.MetaModel_UnknownEntityType, entityType.FullName)); } return table; } /// /// Tries to look up a MetaTable by the entity type. /// /// /// /// public bool TryGetTable(Type entityType, out MetaTable table) { CheckForRegistrationException(); if (entityType == null) { throw new ArgumentNullException("entityType"); } if (!_registerGlobally) { table = Tables.SingleOrDefault(t => t.EntityType == entityType); return table != null; } return MetaModelManager.TryGetTable(entityType, out table); } /// /// Looks up a MetaTable by unique name. Throws if one is not found. The unique name defaults to the table name, or an override /// can be provided via ContextConfiguration when the context that contains the table is registered. The unique name uniquely /// identifies a table within a give MetaModel. It is used for URL generation. /// /// /// public MetaTable GetTable(string uniqueTableName) { CheckForRegistrationException(); MetaTable table; if (!TryGetTable(uniqueTableName, out table)) { throw new ArgumentException(String.Format( CultureInfo.CurrentCulture, DynamicDataResources.MetaModel_UnknownTable, uniqueTableName)); } return table; } /// /// Tries to look up a MetaTable by unique name. Doe /// /// /// /// public bool TryGetTable(string uniqueTableName, out MetaTable table) { CheckForRegistrationException(); if (uniqueTableName == null) { throw new ArgumentNullException("uniqueTableName"); } return _tablesByUniqueName.TryGetValue(uniqueTableName, out table); } /// /// Looks up a MetaTable by the contextType/tableName combination. Throws if one is not found. /// /// /// /// public MetaTable GetTable(string tableName, Type contextType) { CheckForRegistrationException(); if (tableName == null) { throw new ArgumentNullException("tableName"); } if (contextType == null) { throw new ArgumentNullException("contextType"); } MetaTable table; if (!_tablesByContextAndName.TryGetValue(new ContextTypeTableNamePair(contextType, tableName), out table)) { if (!_contextTypes.Contains(contextType)) { throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, DynamicDataResources.MetaModel_UnknownContextType, contextType.FullName)); } else { throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, DynamicDataResources.MetaModel_UnknownTableInContext, contextType.FullName, tableName)); } } return table; } /// /// Lets you set a custom IFieldTemplateFactory. An IFieldTemplateFactor lets you customize which field templates are created /// for the various columns. /// public IFieldTemplateFactory FieldTemplateFactory { get { // If no custom factory was set, use our default if (_fieldTemplateFactory == null) { FieldTemplateFactory = new FieldTemplateFactory(); } return _fieldTemplateFactory; } set { _fieldTemplateFactory = value; // Give the model to the factory if (_fieldTemplateFactory != null) { _fieldTemplateFactory.Initialize(this); } } } public EntityTemplateFactory EntityTemplateFactory { get { if (_entityTemplateFactory == null) { EntityTemplateFactory = new EntityTemplateFactory(); } return _entityTemplateFactory; } set { _entityTemplateFactory = value; if (_entityTemplateFactory != null) { _entityTemplateFactory.Initialize(this); } } } public FilterFactory FilterFactory { get { if (_filterFactory == null) { FilterFactory = new FilterFactory(); } return _filterFactory; } set { _filterFactory = value; if (_filterFactory != null) { _filterFactory.Initialize(this); } } } private string _queryStringKeyPrefix = String.Empty; /// /// Lets you get an action path (URL) to an action for a particular table/action/entity instance combo. /// /// /// /// An object representing a single row of data in a table. Used to provide values for query string parameters. /// public string GetActionPath(string tableName, string action, object row) { return GetTable(tableName).GetActionPath(action, row); } private class ContextTypeTableNamePair : IEquatable { public ContextTypeTableNamePair(Type contextType, string tableName) { Debug.Assert(contextType != null); Debug.Assert(tableName != null); ContextType = contextType; TableName = tableName; HashCode = ContextType.GetHashCode() ^ TableName.GetHashCode(); } private int HashCode { get; set; } public Type ContextType { get; private set; } public string TableName { get; private set; } public bool Equals(ContextTypeTableNamePair other) { if (other == null) { return false; } return ContextType == other.ContextType && TableName.Equals(other.TableName, StringComparison.Ordinal); } public override int GetHashCode() { return HashCode; } public override bool Equals(object obj) { return Equals(obj as ContextTypeTableNamePair); } } internal static class MetaModelManager { private static Hashtable s_modelByContextType = new Hashtable(); private static Hashtable s_tableByEntityType = new Hashtable(); internal static void AddModel(Type contextType, MetaModel model) { Debug.Assert(contextType != null); Debug.Assert(model != null); lock (s_modelByContextType) { s_modelByContextType.Add(contextType, model); } } internal static bool TryGetModel(Type contextType, out MetaModel model) { model = (MetaModel)s_modelByContextType[contextType]; return model != null; } internal static void AddTable(Type entityType, MetaTable table) { Debug.Assert(entityType != null); Debug.Assert(table != null); lock (s_tableByEntityType) { s_tableByEntityType[entityType] = table; } } internal static void Clear() { lock (s_modelByContextType) { s_modelByContextType.Clear(); } lock (s_tableByEntityType) { s_tableByEntityType.Clear(); } } internal static bool TryGetTable(Type type, out MetaTable table) { table = (MetaTable)s_tableByEntityType[type]; return table != null; } } ReadOnlyCollection IMetaModel.Tables { get { return Tables.OfType().ToList().AsReadOnly(); } } bool IMetaModel.TryGetTable(string uniqueTableName, out IMetaTable table) { MetaTable metaTable; table = null; if (TryGetTable(uniqueTableName, out metaTable)) { table = metaTable; return true; } return false; } bool IMetaModel.TryGetTable(Type entityType, out IMetaTable table) { MetaTable metaTable; table = null; if (TryGetTable(entityType, out metaTable)) { table = metaTable; return true; } return false; } List IMetaModel.VisibleTables { get { return VisibleTables.OfType().ToList(); } } IMetaTable IMetaModel.GetTable(string tableName, Type contextType) { return GetTable(tableName, contextType); } IMetaTable IMetaModel.GetTable(string uniqueTableName) { return GetTable(uniqueTableName); } IMetaTable IMetaModel.GetTable(Type entityType) { return GetTable(entityType); } } }