diff --git a/src/Dapper.Contrib/SqlMapperExtensions.Async.cs b/src/Dapper.Contrib/SqlMapperExtensions.Async.cs index c93e39a4..e57d9d29 100644 --- a/src/Dapper.Contrib/SqlMapperExtensions.Async.cs +++ b/src/Dapper.Contrib/SqlMapperExtensions.Async.cs @@ -301,7 +301,7 @@ public static async Task DeleteAsync(this IDbConnection connection, T e } } - var keyProperties = KeyPropertiesCache(type); + var keyProperties = KeyPropertiesCache(type).ToList(); var explicitKeyProperties = ExplicitKeyPropertiesCache(type); if (keyProperties.Count == 0 && explicitKeyProperties.Count == 0) throw new ArgumentException("Entity must have at least one [Key] or [ExplicitKey] property"); diff --git a/src/Dapper.Contrib/SqlMapperExtensions.cs b/src/Dapper.Contrib/SqlMapperExtensions.cs index 9a30e805..da820fbe 100644 --- a/src/Dapper.Contrib/SqlMapperExtensions.cs +++ b/src/Dapper.Contrib/SqlMapperExtensions.cs @@ -52,10 +52,7 @@ public interface ITableNameMapper /// The to get a table name for. public delegate string TableNameMapperDelegate(Type type); - private static readonly ConcurrentDictionary> KeyProperties = new ConcurrentDictionary>(); - private static readonly ConcurrentDictionary> ExplicitKeyProperties = new ConcurrentDictionary>(); - private static readonly ConcurrentDictionary> TypeProperties = new ConcurrentDictionary>(); - private static readonly ConcurrentDictionary> ComputedProperties = new ConcurrentDictionary>(); + private static readonly ConcurrentDictionary TypeProperties = new ConcurrentDictionary(); private static readonly ConcurrentDictionary GetQueries = new ConcurrentDictionary(); private static readonly ConcurrentDictionary TypeTableName = new ConcurrentDictionary(); @@ -71,95 +68,38 @@ private static readonly Dictionary AdapterDictionary ["fbconnection"] = new FbAdapter() }; - private static List ComputedPropertiesCache(Type type) - { - if (ComputedProperties.TryGetValue(type.TypeHandle, out IEnumerable pi)) - { - return pi.ToList(); - } + private static IReadOnlyCollection ComputedPropertiesCache(Type type) => + PropertyInfoCache(type).ComputedProperties; - var computedProperties = TypePropertiesCache(type).Where(p => p.GetCustomAttributes(true).Any(a => a is ComputedAttribute)).ToList(); - - ComputedProperties[type.TypeHandle] = computedProperties; - return computedProperties; - } + private static IReadOnlyCollection ExplicitKeyPropertiesCache(Type type) => + PropertyInfoCache(type).ExplicitKeyProperties; - private static List ExplicitKeyPropertiesCache(Type type) + private static IReadOnlyCollection KeyPropertiesCache(Type type, bool includeId = true) { - if (ExplicitKeyProperties.TryGetValue(type.TypeHandle, out IEnumerable pi)) - { - return pi.ToList(); - } - - var explicitKeyProperties = TypePropertiesCache(type).Where(p => p.GetCustomAttributes(true).Any(a => a is ExplicitKeyAttribute)).ToList(); + var properties = PropertyInfoCache(type); - ExplicitKeyProperties[type.TypeHandle] = explicitKeyProperties; - return explicitKeyProperties; + return includeId + ? properties.KeyProperties + : properties.KeyPropertiesExceptId; } - private static List KeyPropertiesCache(Type type) - { - if (KeyProperties.TryGetValue(type.TypeHandle, out IEnumerable pi)) - { - return pi.ToList(); - } - - var allProperties = TypePropertiesCache(type); - var keyProperties = allProperties.Where(p => p.GetCustomAttributes(true).Any(a => a is KeyAttribute)).ToList(); + private static IReadOnlyCollection TypePropertiesCache(Type type) => PropertyInfoCache(type).AllProperties; - if (keyProperties.Count == 0) - { - var idProp = allProperties.Find(p => string.Equals(p.Name, "id", StringComparison.CurrentCultureIgnoreCase)); - if (idProp != null && !idProp.GetCustomAttributes(true).Any(a => a is ExplicitKeyAttribute)) - { - keyProperties.Add(idProp); - } - } - - KeyProperties[type.TypeHandle] = keyProperties; - return keyProperties; - } - - private static List TypePropertiesCache(Type type) - { - if (TypeProperties.TryGetValue(type.TypeHandle, out IEnumerable pis)) - { - return pis.ToList(); - } - - var properties = type.GetProperties().Where(IsWriteable).ToArray(); - TypeProperties[type.TypeHandle] = properties; - return properties.ToList(); - } - - private static bool IsWriteable(PropertyInfo pi) - { - var attributes = pi.GetCustomAttributes(typeof(WriteAttribute), false).AsList(); - if (attributes.Count != 1) return true; - - var writeAttribute = (WriteAttribute)attributes[0]; - return writeAttribute.Write; - } + private static PropertyInfoWrapper PropertyInfoCache(Type type) => + TypeProperties.GetOrAdd(type.TypeHandle, _ => new PropertyInfoWrapper(type)); private static PropertyInfo GetSingleKey(string method) { var type = typeof(T); - var keys = KeyPropertiesCache(type); - var explicitKeys = ExplicitKeyPropertiesCache(type); - var keyCount = keys.Count + explicitKeys.Count; - if (keyCount > 1) - throw new DataException($"{method} only supports an entity with a single [Key] or [ExplicitKey] property. [Key] Count: {keys.Count}, [ExplicitKey] Count: {explicitKeys.Count}"); - if (keyCount == 0) - throw new DataException($"{method} only supports an entity with a [Key] or an [ExplicitKey] property"); - - return keys.Count > 0 ? keys[0] : explicitKeys[0]; + + return PropertyInfoCache(type).GetSingleKey(method); } /// - /// Returns a single entity by a single id from table "Ts". + /// Returns a single entity by a single id from table "Ts". /// Id must be marked with [Key] attribute. /// Entities created from interfaces are tracked/intercepted for changes and used by the Update() extension - /// for optimal performance. + /// for optimal performance. /// /// Interface or type to create and populate /// Open SqlConnection @@ -698,6 +638,258 @@ private static void CreateProperty(TypeBuilder typeBuilder, string propertyNa typeBuilder.DefineMethodOverride(currSetPropMthdBldr, setMethod); } } + + private class PropertyInfoWrapper + { + [Flags] + private enum PropertyKind + { + None = 0, + Key = 1 << 0, + ExplicitKey = 1 << 1, + Computed = 1 << 2, + NamedId = 1 << 3 + } + + private enum ExceptionKind + { + None = 0, + NoKey, + TooManyKeys + } + + private readonly Lazy> _allProperties; + private readonly Lazy> _keyPropertiesExceptId; + private readonly Lazy _singleKey; + private List _keyProperties = new List(); + private List _explicitKeyProperties = new List(); + private List _computedProperties = new List(); + private PropertyInfo _propertyNamedId; + private ExceptionKind _exceptionKind; + + /// + /// Gets all the properties of the type represented by this instance + /// + public IReadOnlyCollection AllProperties + { + get + { + InitializeProperties(); + + return _allProperties.Value.AsReadOnly(); + } + } + + /// + /// Gets the properties that are decorated with [Key] attribute + /// or the property named "Id" (case insensitive) + /// + public IReadOnlyCollection KeyProperties + { + get + { + InitializeProperties(); + + return _keyProperties.AsReadOnly(); + } + } + + public IReadOnlyCollection KeyPropertiesExceptId => _keyPropertiesExceptId.Value.AsReadOnly(); + + + /// + /// Gets the properties that are decorated with [ExplicitKey] attribute + /// + public IReadOnlyCollection ExplicitKeyProperties + { + get + { + InitializeProperties(); + + return _explicitKeyProperties.AsReadOnly(); + } + } + + /// + /// Gets the properties that are decorated with [Computed] attribute + /// + public IReadOnlyCollection ComputedProperties + { + get + { + InitializeProperties(); + + return _computedProperties.AsReadOnly(); + } + } + + /// + /// Gets the property named "Id" (case insensitive) if one exists, otherwise null + /// + public PropertyInfo PropertyNamedId => _propertyNamedId; + + public PropertyInfoWrapper(Type type) + { + _allProperties = new Lazy>(() => GetAllProperties(type).ToList()); + _keyPropertiesExceptId = new Lazy>(() => KeyProperties.Where(p => !IsPropertyNamedId(p)).ToList()); + _singleKey = new Lazy(() => GetSingleKey(out _exceptionKind)); + } + + public PropertyInfo GetSingleKey(string method) + { + if (_singleKey.Value != null) + { + return _singleKey.Value; + } + + var exception = GetException(_exceptionKind, method); + + throw exception; + } + + private static bool IsWriteable(PropertyInfo pi) + { + var attributes = pi.GetCustomAttributes(typeof(WriteAttribute), false).AsList(); + if (attributes.Count != 1) return true; + + var writeAttribute = (WriteAttribute)attributes[0]; + return writeAttribute.Write; + } + + private void InitializeProperties() + { + _ = _allProperties.Value; + } + + private IEnumerable GetAllProperties(Type type) + { + var allProperties = type.GetProperties().Where(IsWriteable).ToArray(); + + AssignPropertyInfo(allProperties, + ref _keyProperties, + ref _explicitKeyProperties, + ref _computedProperties, + ref _propertyNamedId); + + return allProperties; + } + + private PropertyInfo GetSingleKey(out ExceptionKind exceptionKind) + { + var keyCount = KeyProperties.Count + ExplicitKeyProperties.Count; + switch (keyCount) + { + case 0: + exceptionKind = ExceptionKind.NoKey; + + return null; + case 1: + exceptionKind = ExceptionKind.None; + + return KeyProperties.Count > 0 ? KeyProperties.FirstOrDefault() : ExplicitKeyProperties.FirstOrDefault(); + case 2 when KeyPropertiesExceptId.Count == 1: + exceptionKind = ExceptionKind.None; + + return KeyPropertiesExceptId.FirstOrDefault(); + default: + exceptionKind = ExceptionKind.TooManyKeys; + + return null; + } + } + + private DataException GetException(ExceptionKind exceptionKind, string method) => + exceptionKind switch + { + ExceptionKind.None => throw new InvalidOperationException(), + ExceptionKind.NoKey => GetNoKeyException(method), + ExceptionKind.TooManyKeys => GetTooManyKeysException(method), + var _ => throw new ArgumentOutOfRangeException(nameof(exceptionKind), exceptionKind, null) + }; + + private DataException GetTooManyKeysException(string method) => + new("{method} only supports an entity with a single [Key] or [ExplicitKey] property. [Key] Count: {KeyProperties.Count}, [ExplicitKey] Count: {ExplicitKeyProperties.Count}"); + + private static DataException GetNoKeyException(string method) => + new($"{method} only supports an entity with a [Key] or an [ExplicitKey] property"); + + private static void AssignPropertyInfo(IEnumerable properties, + ref List keys, + ref List explicitKeys, + ref List computedProperties, + ref PropertyInfo propertyNamedId) + { + foreach (var propertyInfo in properties) + { + var propertyKind = GetPropertyKind(propertyInfo); + + if (propertyKind.HasFlag(PropertyKind.Key)) + { + keys.Add(propertyInfo); + } + + if (propertyKind.HasFlag(PropertyKind.ExplicitKey)) + { + explicitKeys.Add(propertyInfo); + } + + if (propertyKind.HasFlag(PropertyKind.Computed)) + { + computedProperties.Add(propertyInfo); + } + + if (propertyKind.HasFlag(PropertyKind.NamedId)) + { + propertyNamedId ??= propertyInfo; + + if (!propertyKind.HasFlag(PropertyKind.ExplicitKey) && !propertyKind.HasFlag(PropertyKind.Key)) + { + keys.Add(propertyInfo); + } + } + } + } + + private static PropertyKind GetPropertyKind(PropertyInfo propertyInfo) + { + var propertyKind = GetPropertyKindFromAttribute(propertyInfo); + + if (IsPropertyNamedId(propertyInfo)) + { + propertyKind = PropertyKind.NamedId; + } + + return propertyKind; + } + + private static bool IsPropertyNamedId(PropertyInfo propertyInfo) => + string.Equals(propertyInfo.Name, "id", StringComparison.CurrentCultureIgnoreCase); + + private static PropertyKind GetPropertyKindFromAttribute(PropertyInfo propertyInfo) + { + var customAttributes = propertyInfo.GetCustomAttributes(true); + var propertyKind = PropertyKind.None; + + foreach (var customAttribute in customAttributes) + switch (customAttribute) + { + case KeyAttribute: + propertyKind |= PropertyKind.Key; + + break; + case ExplicitKeyAttribute: + propertyKind |= PropertyKind.ExplicitKey; + + break; + case ComputedAttribute: + propertyKind |= PropertyKind.Computed; + + break; + } + + return propertyKind; + } + } } ///