File: System\Data\Services\Client\Binding\BindingEntityInfo.cs
Project: ndp\fx\src\DataWeb\Client\System.Data.Services.Client.csproj (System.Data.Services.Client)
//---------------------------------------------------------------------
// <copyright file="BindingEntityInfo.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <summary>
//   BindingEntityInfo class
// </summary>
//
//---------------------------------------------------------------------
 
namespace System.Data.Services.Client
{
#region Namespaces
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data.Services.Common;
    using System.Diagnostics;
    using System.Linq;
    using System.Reflection;
    using System.Threading;
#endregion
 
    /// <summary>Type of property stored in BindingPropertyInfo.</summary>
    internal enum BindingPropertyKind
    {
        /// <summary>Property type is a complex type.</summary>
        BindingPropertyKindComplex,
 
        /// <summary>Property type is an entity type with keys.</summary>
        BindingPropertyKindEntity,
 
        /// <summary>Property is of an DataServiceCollection.</summary>
        BindingPropertyKindCollection
    }
 
    /// <summary>Cache of information about entity types and their observable properties</summary>
    internal class BindingEntityInfo
    {
        /// <summary>Object reference used as a 'False' flag.</summary>
        private static readonly object FalseObject = new object();
 
        /// <summary>Object reference used as a 'False' flag.</summary>
        private static readonly object TrueObject = new object();
 
        /// <summary>Lock on metadata caches.</summary>
        private static readonly ReaderWriterLockSlim metadataCacheLock = new ReaderWriterLockSlim();
 
        /// <summary>Types which are known not to be entity types.</summary>
        private static readonly HashSet<Type> knownNonEntityTypes = new HashSet<Type>(EqualityComparer<Type>.Default);
 
        /// <summary>Types which are known to be (or not) collection types.</summary>
        private static readonly Dictionary<Type, object> knownObservableCollectionTypes = new Dictionary<Type, object>(EqualityComparer<Type>.Default);
 
        /// <summary>Mapping between types and their corresponding entity information</summary>
        private static readonly Dictionary<Type, BindingEntityInfoPerType> bindingEntityInfos = new Dictionary<Type, BindingEntityInfoPerType>(EqualityComparer<Type>.Default);
 
        /// <summary>Obtain binding info corresponding to a given type</summary>
        /// <param name="entityType">Type for which to obtain information</param>
        /// <returns>Info about the <paramref name="entityType"/></returns>
        internal static IList<BindingPropertyInfo> GetObservableProperties(Type entityType)
        {
            return GetBindingEntityInfoFor(entityType).ObservableProperties;
        }
 
        /// <summary>Gets the ClientType corresponding to the given type</summary>
        /// <param name="entityType">Input type</param>
        /// <returns>Corresponding ClientType</returns>
        internal static ClientType GetClientType(Type entityType)
        {
            return GetBindingEntityInfoFor(entityType).ClientType;
        }
 
        /// <summary>
        /// Get the entity set name for the target entity object.
        /// </summary>
        /// <param name="target">An entity object.</param>
        /// <param name="targetEntitySet">The 'currently known' entity set name for the target object.</param>
        /// <returns>The entity set name for the target object.</returns>
        /// <remarks>
        /// Allow user code to provide the entity set name. If user code does not provide the entity set name, then 
        /// this method will get the entity set name from the value of the EntitySetAttribute.
        /// The 'currently known' entity set name for top level collections can be provided through OEC constructor
        /// </remarks>
        internal static string GetEntitySet(
            object target,
            string targetEntitySet)
        {
            Debug.Assert(target != null, "Argument 'target' cannot be null.");
            Debug.Assert(BindingEntityInfo.IsEntityType(target.GetType()), "Argument 'target' must be an entity type.");
 
            // Here's the rules in order of priority for resolving entity set name
            // 1. EntitySet name passed in the constructor or extension methods of DataServiceCollection
            // 2. EntitySet name specified in the EntitySet attribute by the code gen. {Remember this attribute is
            //    not generated in case of MEST)
            if (!String.IsNullOrEmpty(targetEntitySet))
            {
                return targetEntitySet;
            }
            else
            {
                // If there is not a 'currently known' entity set name to validate against, then there must be 
                // EntitySet attribute on the entity type
                return BindingEntityInfo.GetEntitySetAttribute(target.GetType());
            }
        }
 
        /// <summary>
        /// Determine if the specified type is an DataServiceCollection.
        /// </summary>
        /// <remarks>
        /// If there a generic class in the inheritance hierarchy of the type, that has a single
        /// entity type paramenter T, and is assignable to DataServiceCollection(Of T), then
        /// the type is an DataServiceCollection.
        /// </remarks>
        /// <param name="collectionType">An object type specifier.</param>
        /// <returns>True if the type is an DataServiceCollection; otherwise false.</returns>
        internal static bool IsDataServiceCollection(Type collectionType)
        {
            Debug.Assert(collectionType != null, "Argument 'collectionType' cannot be null.");
 
            metadataCacheLock.EnterReadLock();
            try
            {
                object resultAsObject;
                if (knownObservableCollectionTypes.TryGetValue(collectionType, out resultAsObject))
                {
                    return resultAsObject == TrueObject;
                }
            }
            finally
            {
                metadataCacheLock.ExitReadLock();
            }
 
            Type type = collectionType;
            bool result = false;
 
            while (type != null)
            {
                if (type.IsGenericType)
                {
                    // Is there a generic class in the inheritance hierarchy, that has a single
                    // entity type paramenter T, and is assignable to DataServiceCollection<T>
                    Type[] parms = type.GetGenericArguments();
 
                    if (parms != null && parms.Length == 1 && IsEntityType(parms[0]))
                    {
                        // if ObservableCollection is not available dataServiceCollection will be null
                        Type dataServiceCollection = WebUtil.GetDataServiceCollectionOfT(parms);
                        if (dataServiceCollection != null && dataServiceCollection.IsAssignableFrom(type))
                        {
                            result = true;
                            break;
                        }
                    }
                }
 
                type = type.BaseType;
            }
 
            metadataCacheLock.EnterWriteLock();
            try
            {
                if (!knownObservableCollectionTypes.ContainsKey(collectionType))
                {
                    knownObservableCollectionTypes[collectionType] = result ? TrueObject : FalseObject;
                }
            }
            finally
            {
                metadataCacheLock.ExitWriteLock();
            }
 
            return result;
        }
 
        /// <summary>
        /// Determine if the specified type is an entity type.
        /// </summary>
        /// <param name="type">An object type specifier.</param>
        /// <returns>True if the type is an entity type; otherwise false.</returns>
        internal static bool IsEntityType(Type type)
        {
            Debug.Assert(type != null, "Argument 'type' cannot be null.");
 
            metadataCacheLock.EnterReadLock();
            try
            {
                if (knownNonEntityTypes.Contains(type))
                {
                    return false;
                }
            }
            finally
            {
                metadataCacheLock.ExitReadLock();
            }
 
            try
            {
                if (BindingEntityInfo.IsDataServiceCollection(type))
                {
                    return false;
                }
 
                return ClientType.Create(type).IsEntityType;
            }
            catch (InvalidOperationException)
            {
                metadataCacheLock.EnterWriteLock();
                try
                {
                    if (!knownNonEntityTypes.Contains(type))
                    {
                        knownNonEntityTypes.Add(type);
                    }
                }
                finally
                {
                    metadataCacheLock.ExitWriteLock();
                }
 
                return false;
            }
        }
 
        /// <summary>
        /// Gets the value of a property and corresponding BindingPropertyInfo if property is being observed
        /// </summary>
        /// <param name="source">Source object whose property needs to be read</param>
        /// <param name="sourceProperty">Name of the source object property</param>
        /// <param name="bindingPropertyInfo">BindingPropertyInfo corresponding to <paramref name="sourceProperty"/></param>
        /// <returns>Value of the property</returns>
        internal static object GetPropertyValue(object source, string sourceProperty, out BindingPropertyInfo bindingPropertyInfo)
        {
            Type sourceType = source.GetType();
 
            bindingPropertyInfo = BindingEntityInfo.GetObservableProperties(sourceType)
                                                   .SingleOrDefault(x => x.PropertyInfo.PropertyName == sourceProperty);
 
            // bindingPropertyInfo is null for primitive properties.
            if (bindingPropertyInfo == null)
            {
                return BindingEntityInfo.GetClientType(sourceType)
                                        .GetProperty(sourceProperty, false)
                                        .GetValue(source);
            }
            else
            {
                return bindingPropertyInfo.PropertyInfo.GetValue(source);
            }
        }
 
        /// <summary>Obtain binding info corresponding to a given type</summary>
        /// <param name="entityType">Type for which to obtain information</param>
        /// <returns>Info about the <paramref name="entityType"/></returns>
        private static BindingEntityInfoPerType GetBindingEntityInfoFor(Type entityType)
        {
            BindingEntityInfoPerType bindingEntityInfo;
 
            metadataCacheLock.EnterReadLock();
            try
            {
                if (bindingEntityInfos.TryGetValue(entityType, out bindingEntityInfo))
                {
                    return bindingEntityInfo;
                }
            }
            finally
            {
                metadataCacheLock.ExitReadLock();
            }
 
            bindingEntityInfo = new BindingEntityInfoPerType();
 
            // Try to get the entity set name from the EntitySetAttribute attributes. In order to make the
            // inheritance work, we need to look at the attributes declared in the base types also.
            object[] attributes = entityType.GetCustomAttributes(typeof(EntitySetAttribute), true);
 
            // There must be exactly one (unambiguous) EntitySetAttribute attribute.
            bindingEntityInfo.EntitySet = (attributes != null && attributes.Length == 1) ? ((EntitySetAttribute)attributes[0]).EntitySet : null;
            bindingEntityInfo.ClientType = ClientType.Create(entityType);
            
            foreach (ClientType.ClientProperty p in bindingEntityInfo.ClientType.Properties)
            {
                BindingPropertyInfo bpi = null;
            
                Type propertyType = p.PropertyType;
                
                if (p.CollectionType != null)
                {
                    if (BindingEntityInfo.IsDataServiceCollection(propertyType))
                    {
                        bpi = new BindingPropertyInfo { PropertyKind = BindingPropertyKind.BindingPropertyKindCollection };
                    }
                }
                else
                if (BindingEntityInfo.IsEntityType(propertyType))
                {
                    bpi = new BindingPropertyInfo { PropertyKind = BindingPropertyKind.BindingPropertyKindEntity };
                }
                else
                if (BindingEntityInfo.CanBeComplexProperty(p))
                {
                    // Add complex types and nothing else.
                    bpi = new BindingPropertyInfo { PropertyKind = BindingPropertyKind.BindingPropertyKindComplex };
                }
                
                if (bpi != null)
                {
                    bpi.PropertyInfo = p;
                    
                    // For complex types only treat complex typed properties as observable, we are not going to observer entity typed or primitive properties.
                    if (bindingEntityInfo.ClientType.IsEntityType || bpi.PropertyKind == BindingPropertyKind.BindingPropertyKindComplex)
                    {
                        bindingEntityInfo.ObservableProperties.Add(bpi);
                    }
                }
            }
 
            metadataCacheLock.EnterWriteLock();
            try
            {
                if (!bindingEntityInfos.ContainsKey(entityType))
                {
                    bindingEntityInfos[entityType] = bindingEntityInfo;
                }
            }
            finally
            {
                metadataCacheLock.ExitWriteLock();
            }
 
            return bindingEntityInfo;
        }
 
        /// <summary>Checks whether a given property can be a complex property i.e. implements INotifyPropertyChanged.</summary>
        /// <param name="property">Input property.</param>
        /// <returns>true if the property is complex property, false otherwise.</returns>
        private static bool CanBeComplexProperty(ClientType.ClientProperty property)
        {
            Debug.Assert(property != null, "property != null");
            if (typeof(INotifyPropertyChanged).IsAssignableFrom(property.PropertyType))
            {
                Debug.Assert(!property.IsKnownType, "Known types do not implement INotifyPropertyChanged.");
                return true;
            }
 
            return false;
        }
 
        /// <summary>Gets entity set corresponding to a given type</summary>
        /// <param name="entityType">Intput type</param>
        /// <returns>Entity set name for the type</returns>
        private static string GetEntitySetAttribute(Type entityType)
        {
            return GetBindingEntityInfoFor(entityType).EntitySet;
        }
 
        /// <summary>Information about a property interesting for binding</summary>
        internal class BindingPropertyInfo
        {
            /// <summary>Property information</summary>
            public ClientType.ClientProperty PropertyInfo
            {
                get;
                set;
            }
 
            /// <summary>Kind of the property i.e. complex, entity or collection.</summary>
            public BindingPropertyKind PropertyKind
            {
                get;
                set;
            }
        }
 
        /// <summary>Holder of information about entity properties for a type</summary>
        private sealed class BindingEntityInfoPerType
        {
            /// <summary>Collection of properties interesting to the observer</summary>
            private List<BindingPropertyInfo> observableProperties;
 
            /// <summary>Constructor</summary>
            public BindingEntityInfoPerType()
            {
                this.observableProperties = new List<BindingPropertyInfo>();
            }
 
            /// <summary>Entity set of the entity</summary>
            public String EntitySet
            {
                get;
                set;
            }
 
            /// <summary>Corresponding ClientTyp</summary>
            public ClientType ClientType
            {
                get;
                set;
            }
 
            /// <summary>Collection of properties interesting to the observer</summary>
            public List<BindingPropertyInfo> ObservableProperties
            {
                get
                {
                    return this.observableProperties;
                }
            }
        }
 
#if ASTORIA_LIGHT
        /// <summary>Read-writer lock, implemented over a Monitor.</summary>
        private sealed class ReaderWriterLockSlim
        {
            /// <summary>Single object on which to lock.</summary>
            private object _lock = new object();
 
            /// <summary>Enters a reader lock. Writers will also be blocked.</summary>            
            internal void EnterReadLock()
            {
                Monitor.Enter(_lock);
            }
 
            /// <summary>Enters a writer lock. Readers will also be blocked.</summary>
            internal void EnterWriteLock()
            {
                Monitor.Enter(_lock);
            }
 
            /// <summary>Exits a reader lock.</summary>
            internal void ExitReadLock()
            {
                Monitor.Exit(_lock);
            }
 
            /// <summary>Exits a writer lock.</summary>
            internal void ExitWriteLock()
            {
                Monitor.Exit(_lock);
            }
        }
#endif
    }
}