File: DynamicData\MetaModel.cs
Project: ndp\fx\src\xsp\system\DynamicData\System.Web.DynamicData.csproj (System.Web.DynamicData)
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 {
    /// <summary>
    /// 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.
    /// </summary>
    public class MetaModel : IMetaModel {
        private List<Type> _contextTypes = new List<Type>();
        private static object _lock = new object();
        private List<MetaTable> _tables = new List<MetaTable>();
        private ReadOnlyCollection<MetaTable> _tablesRO;
        private Dictionary<string, MetaTable> _tablesByUniqueName = new Dictionary<string, MetaTable>(StringComparer.OrdinalIgnoreCase);
        private Dictionary<ContextTypeTableNamePair, MetaTable> _tablesByContextAndName = new Dictionary<ContextTypeTableNamePair, MetaTable>();
        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<Type, bool> s_registeredMetadataTypes = new ConcurrentDictionary<Type, bool>();
 
        // Use global registration is true by default
        private bool _registerGlobally = true;
 
        internal virtual int RegisteredDataModelsCount {
            get {
                return _contextTypes.Count;
            }
        }
 
        /// <summary>
        /// ctor
        /// </summary>
        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<MetaTable>(_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;
            }
        }
 
        /// <summary>
        /// allows for setting of the DynamicData folder for this mode. The default is ~/DynamicData/
        /// </summary>
        public string DynamicDataFolderVirtualPath {
            get {
                if (_dynamicDataFolderVirtualPath == null) {
                    _dynamicDataFolderVirtualPath = "~/DynamicData/";
                }
 
                return _dynamicDataFolderVirtualPath;
            }
            set {
                // Make sure it ends with a slash
                _dynamicDataFolderVirtualPath = VirtualPathUtility.AppendTrailingSlash(value);
            }
        }
 
        /// <summary>
        /// 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.
        /// </summary>
        public static MetaModel Default {
            get {
                CheckForRegistrationException();
                return s_defaultModel;
            }
            internal set { s_defaultModel = value; }
        }
 
        /// <summary>
        /// Gets the model instance that had the contextType registered with it
        /// </summary>
        /// <param name="contextType">A DataContext or ObjectContext type (e.g. NorthwindDataContext)</param>
        /// <returns>a model</returns>
        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));
            }
        }
 
        /// <summary>
        /// Registers a context. Uses the default ContextConfiguration options.
        /// </summary>
        /// <param name="contextType"></param>
        public void RegisterContext(Type contextType) {
            RegisterContext(contextType, new ContextConfiguration());
        }
 
        /// <summary>
        /// Registers a context. Uses the the given ContextConfiguration options.
        /// </summary>
        /// <param name="contextType"></param>
        /// <param name="configuration"></param>
        public void RegisterContext(Type contextType, ContextConfiguration configuration) {
            if (contextType == null) {
                throw new ArgumentNullException("contextType");
            }
            RegisterContext(() => Activator.CreateInstance(contextType), configuration);
        }
 
        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="contextFactory"></param>
        public void RegisterContext(Func<object> contextFactory) {
            RegisterContext(contextFactory, new ContextConfiguration());
        }
 
        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="contextFactory"></param>
        /// <param name="configuration"></param>
        public void RegisterContext(Func<object> 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);
        }
 
        /// <summary>
        /// Register context using give model provider. Uses default context configuration.
        /// </summary>
        /// <param name="dataModelProvider"></param>
        public void RegisterContext(DataModelProvider dataModelProvider) {
            RegisterContext(dataModelProvider, new ContextConfiguration());
        }
 
        /// <summary>
        /// Register context using give model provider. Uses given context configuration.
        /// </summary>
        /// <param name="dataModelProvider"></param>
        /// <param name="configuration"></param>
        [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<TableProvider> tableProviders = dataModelProvider.Tables;
 
                // create and validate model
                var tablesToInitialize = new List<MetaTable>();
                foreach (TableProvider tableProvider in tableProviders) {
                    RegisterMetadataTypeDescriptionProvider(tableProvider, configuration.MetadataProviderFactory);
 
                    MetaTable table = CreateTable(tableProvider);
                    table.CreateColumns();
 
                    var tableNameAttribute = tableProvider.Attributes.OfType<TableNameAttribute>().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);
            }
        }
 
        /// <summary>
        /// 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.
        /// </summary>
        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;
        }
 
        /// <summary>
        /// Instantiate a MetaTable object. Can be overridden to instantiate a derived type 
        /// </summary>
        /// <returns></returns>
        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<MetaTable> 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<Type, TypeDescriptionProvider> 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);
                    }
                }
            }
        }
 
        /// <summary>
        /// Returns a collection of all the tables that are part of the context, regardless of whether they are visible or not.
        /// </summary>
        public ReadOnlyCollection<MetaTable> Tables {
            get {
                CheckForRegistrationException();
                return _tablesRO;
            }
        }
 
        /// <summary>
        /// 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
        /// </summary>
        public List<MetaTable> 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);
        }
 
        /// <summary>
        /// Looks up a MetaTable by the entity type. Throws an exception if one is not found.
        /// </summary>
        /// <param name="entityType"></param>
        /// <returns></returns>
        [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;
        }
 
        /// <summary>
        /// Tries to look up a MetaTable by the entity type.
        /// </summary>
        /// <param name="entityType"></param>
        /// <param name="table"></param>
        /// <returns></returns>
        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);
        }
 
        /// <summary>
        /// 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.
        /// </summary>
        /// <param name="uniqueTableName"></param>
        /// <returns></returns>
        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;
        }
 
        /// <summary>
        /// Tries to look up a MetaTable by unique name. Doe
        /// </summary>
        /// <param name="uniqueTableName"></param>
        /// <param name="table"></param>
        /// <returns></returns>
        public bool TryGetTable(string uniqueTableName, out MetaTable table) {
            CheckForRegistrationException();
 
            if (uniqueTableName == null) {
                throw new ArgumentNullException("uniqueTableName");
            }
 
            return _tablesByUniqueName.TryGetValue(uniqueTableName, out table);
        }
 
        /// <summary>
        /// Looks up a MetaTable by the contextType/tableName combination. Throws if one is not found.
        /// </summary>
        /// <param name="tableName"></param>
        /// <param name="contextType"></param>
        /// <returns></returns>
        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;
        }
 
        /// <summary>
        /// Lets you set a custom IFieldTemplateFactory. An IFieldTemplateFactor lets you customize which field templates are created
        /// for the various columns.
        /// </summary>
        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;
 
        /// <summary>
        /// Lets you get an action path (URL) to an action for a particular table/action/entity instance combo.
        /// </summary>
        /// <param name="tableName"></param>
        /// <param name="action"></param>
        /// <param name="row">An object representing a single row of data in a table. Used to provide values for query string parameters.</param>
        /// <returns></returns>
        public string GetActionPath(string tableName, string action, object row) {
            return GetTable(tableName).GetActionPath(action, row);
        }
 
        private class ContextTypeTableNamePair : IEquatable<ContextTypeTableNamePair> {
            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<IMetaTable> IMetaModel.Tables {
            get {
                return Tables.OfType<IMetaTable>().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<IMetaTable> IMetaModel.VisibleTables {
            get {
                return VisibleTables.OfType<IMetaTable>().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);
        }
    }
}