/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Globalization; using System.Linq; using System.Reflection; using System.Text; using System.Threading; using Amazon.DynamoDBv2.DocumentModel; using Amazon.DynamoDBv2.Model; using Amazon.Runtime; using Amazon.Runtime.Internal.Util; using Amazon.Util; using Amazon.Util.Internal; namespace Amazon.DynamoDBv2.DataModel { /// /// Basic property storage information /// internal class SimplePropertyStorage { // local property name public string PropertyName { get; protected set; } // DynamoDB attribute name public string AttributeName { get; set; } // MemberInfo of the property public MemberInfo Member { get; protected set; } // Type of the property public Type MemberType { get; protected set; } // Converter type, if one is present public Type ConverterType { get; set; } // Converter, if one is present public IPropertyConverter Converter { get; protected set; } public SimplePropertyStorage(MemberInfo member) : this(Utils.GetType(member)) { Member = member; PropertyName = member.Name; } public SimplePropertyStorage(Type memberType) { MemberType = memberType; } public override string ToString() { return string.Format(CultureInfo.InvariantCulture, "{0} {1}", MemberType.FullName, PropertyName); } } /// /// DynamoDB property storage information /// internal class PropertyStorage : SimplePropertyStorage { // flags public bool IsHashKey { get; set; } public bool IsRangeKey { get; set; } public bool IsKey { get { return IsHashKey || IsRangeKey; } } public bool IsVersion { get; set; } public bool IsLSIRangeKey { get; set; } public bool IsGSIHashKey { get; set; } public bool IsGSIRangeKey { get; set; } public bool IsGSIKey { get { return IsGSIHashKey || IsGSIRangeKey; } } public bool IsIgnored { get; set; } // whether to store DateTime as epoch seconds integer public bool StoreAsEpoch { get; set; } // corresponding IndexNames, if applicable public List IndexNames { get; set; } public void AddIndex(DynamoDBGlobalSecondaryIndexHashKeyAttribute gsiHashKey) { AddIndex(new GSI(true, gsiHashKey.AttributeName, gsiHashKey.IndexNames)); } public void AddIndex(DynamoDBGlobalSecondaryIndexRangeKeyAttribute gsiRangeKey) { AddIndex(new GSI(false, gsiRangeKey.AttributeName, gsiRangeKey.IndexNames)); } public void AddIndex(DynamoDBLocalSecondaryIndexRangeKeyAttribute lsiRangeKey) { AddIndex(new LSI(lsiRangeKey.AttributeName, lsiRangeKey.IndexNames)); } public void AddGsiIndex(bool isHashKey, string attributeName, params string[] indexNames) { AddIndex(new GSI(isHashKey, attributeName, indexNames)); } public void AddLsiIndex(string attributeName, params string[] indexNames) { AddIndex(new LSI(attributeName, indexNames)); } public void AddIndex(Index index) { Indexes.Add(index); } public List Indexes { get; private set; } public abstract class Index { public List IndexNames { get; private set; } public string AttributeName { get; private set; } public Index(string attributeName, params string[] indexNames) { IndexNames = new List(indexNames); AttributeName = attributeName; } } public class LSI : Index { public LSI(string attributeName, params string[] indexNames) : base(attributeName, indexNames) { } } public class GSI : Index { public bool IsHashKey { get; private set; } public GSI(bool isHashKey, string attributeName, params string[] indexNames) : base(attributeName, indexNames) { IsHashKey = isHashKey; } } /// /// Validates configurations and sets required fields /// public void Validate(DynamoDBContext context) { if (IsVersion) Utils.ValidateVersionType(MemberType); // no conversion is possible, so type must be a nullable primitive if (IsHashKey && IsRangeKey) throw new InvalidOperationException("Property " + PropertyName + " cannot be both hash and range key"); if (ConverterType != null) { if (StoreAsEpoch) throw new InvalidOperationException("Converter for " + PropertyName + " must not be set at the same time as StoreAsEpoch is set to true"); if (!Utils.CanInstantiateConverter(ConverterType) || !Utils.ImplementsInterface(ConverterType, typeof(IPropertyConverter))) throw new InvalidOperationException("Converter for " + PropertyName + " must be instantiable with no parameters and must implement IPropertyConverter"); this.Converter = Utils.InstantiateConverter(ConverterType, context) as IPropertyConverter; } IPropertyConverter converter; if (context.ConverterCache.TryGetValue(MemberType, out converter) && converter != null) { this.Converter = converter; } foreach (var index in Indexes) IndexNames.AddRange(index.IndexNames); } public PropertyStorage(MemberInfo member) : base(member) { IndexNames = new List(); Indexes = new List(); } } /// /// Storage information for a single item /// internal class ItemStorage { public Document Document { get; set; } public ItemStorageConfig Config { get; set; } public Primitive CurrentVersion { get; set; } public HashSet ConvertedObjects { get; private set; } public ItemStorage(ItemStorageConfig storageConfig) { Document = new Document(); Config = storageConfig; ConvertedObjects = new HashSet(); } } /// /// GSI info /// internal class GSIConfig { public GSIConfig(string indexName) { IndexName = indexName; } // index name public string IndexName { get; set; } // keys public string HashKeyPropertyName { get; set; } public string RangeKeyPropertyName { get; set; } } /// /// Storage information for a specific class /// internal class StorageConfig { // normalized PropertyStorage objects public List Properties { get; private set; } // target type public ITypeInfo TargetTypeInfo { get; private set; } // target type members public Dictionary TargetTypeMembers { get; private set; } // storage mappings private Dictionary PropertyToPropertyStorageMapping { get; set; } protected void AddPropertyStorage(string propertyName, PropertyStorage propertyStorage) { PropertyToPropertyStorageMapping[propertyName] = propertyStorage; } public PropertyStorage GetPropertyStorage(string propertyName) { PropertyStorage storage; if (TryGetPropertyStorage(propertyName, out storage)) return storage; throw new InvalidOperationException("Unable to find storage information for property [" + propertyName + "]"); } public bool TryGetPropertyStorage(string propertyName, out PropertyStorage storage) { return (PropertyToPropertyStorageMapping.TryGetValue(propertyName, out storage)); } public IEnumerable AllPropertyStorage { get { return PropertyToPropertyStorageMapping.Values; } } public bool FindPropertyByPropertyName(string propertyName, out PropertyStorage propertyStorage) { return FindSingleProperty( p => string.Equals(p.PropertyName, propertyName, StringComparison.Ordinal), "Multiple properties configured for property " + propertyName, out propertyStorage); } public bool FindSinglePropertyByAttributeName(string attributeName, out PropertyStorage propertyStorage) { return FindSingleProperty( p => string.Equals(p.AttributeName, attributeName, StringComparison.Ordinal), "Multiple properties configured for attribute " + attributeName, out propertyStorage); } private bool FindSingleProperty(Func match, string errorMessage, out PropertyStorage propertyStorage) { var properties = Properties .Where(ps => !ps.IsIgnored) .Where(match) .ToList(); if (properties.Count == 0) { propertyStorage = null; return false; } if (properties.Count == 1) { propertyStorage = properties[0]; return true; } throw new InvalidOperationException(errorMessage); } public static bool IsValidMemberInfo(MemberInfo member) { // filter out non-fields and non-properties if (!(member is FieldInfo || member is PropertyInfo)) return false; // filter out properties that aren't both read and write if (!Utils.IsReadWrite(member)) return false; return true; } private static Dictionary GetMembersDictionary(ITypeInfo typeInfo) { Dictionary dictionary = new Dictionary(StringComparer.Ordinal); var members = typeInfo .GetMembers() .Where(IsValidMemberInfo) .ToList(); foreach (var member in members) { InternalSDKUtils.AddToDictionary(dictionary, member.Name, member); } return dictionary; } // constructor public StorageConfig(ITypeInfo targetTypeInfo) { var type = targetTypeInfo.Type; if (!Utils.CanInstantiate(type)) throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Type {0} is unsupported, it cannot be instantiated", targetTypeInfo.FullName)); TargetTypeInfo = targetTypeInfo; Properties = new List(); PropertyToPropertyStorageMapping = new Dictionary(StringComparer.Ordinal); TargetTypeMembers = GetMembersDictionary(targetTypeInfo); if (TargetTypeMembers.Count == 0) throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Type {0} is unsupported, it has no supported members", targetTypeInfo.FullName)); } } /// /// Storage information for a specific class that is associated with a table /// internal class ItemStorageConfig : StorageConfig { // table public string TableName { get; set; } public bool LowerCamelCaseProperties { get; set; } public HashSet AttributesToStoreAsEpoch { get; set; } // keys public List HashKeyPropertyNames { get; private set; } public List RangeKeyPropertyNames { get; private set; } // properties to get public List AttributesToGet { get; private set; } // version public string VersionPropertyName { get; private set; } public bool HasVersion { get { return !string.IsNullOrEmpty(VersionPropertyName); } } // attribute-to-index mapping public Dictionary> AttributeToIndexesNameMapping { get; set; } // indexName to LSI range key properties mapping public Dictionary> IndexNameToLSIRangePropertiesMapping { get; set; } // indexName to GSIConfig mapping public Dictionary IndexNameToGSIMapping { get; set; } //public void RemovePropertyStorage(string propertyName) //{ // PropertyStorage storage; // if (!TryGetPropertyStorage(propertyName, out storage)) // return; // string attributeName = storage.AttributeName; // // Remove all references // if (storage.IsHashKey) // HashKeyPropertyNames.Remove(propertyName); // if (storage.IsRangeKey) // RangeKeyPropertyNames.Remove(propertyName); // if (storage.IsVersion) // VersionPropertyName = null; // if (storage.IsGSIHashKey) // IndexNameToGSIMapping // AttributesToGet.Remove(attributeName); // PropertyToPropertyStorageMapping.Remove(propertyName); //} public GSIConfig GetGSIConfig(string indexName) { GSIConfig gsiConfig; if (!this.IndexNameToGSIMapping.TryGetValue(indexName, out gsiConfig)) gsiConfig = null; return gsiConfig; } public string GetCorrectHashKeyProperty(DynamoDBFlatConfig currentConfig, string hashKeyProperty) { if (currentConfig.IsIndexOperation) { string indexName = currentConfig.IndexName; GSIConfig gsiConfig = this.GetGSIConfig(indexName); // Use GSI hash key if GSI is found AND GSI hash-key is set if (gsiConfig != null && !string.IsNullOrEmpty(gsiConfig.HashKeyPropertyName)) hashKeyProperty = gsiConfig.HashKeyPropertyName; } return hashKeyProperty; } public string GetRangeKeyByIndex(string indexName) { string rangeKeyPropertyName = null; // test LSI first List rangeProperties; if (IndexNameToLSIRangePropertiesMapping.TryGetValue(indexName, out rangeProperties) && rangeProperties != null && rangeProperties.Count == 1) { rangeKeyPropertyName = rangeProperties[0]; } GSIConfig gsiConfig = GetGSIConfig(indexName); if (gsiConfig != null) { rangeKeyPropertyName = gsiConfig.RangeKeyPropertyName; } if (string.IsNullOrEmpty(rangeKeyPropertyName)) throw new InvalidOperationException("Unable to determine range key from index name"); return rangeKeyPropertyName; } public PropertyStorage VersionPropertyStorage { get { if (!HasVersion) throw new InvalidOperationException("No version field defined for this type"); return GetPropertyStorage(VersionPropertyName); } } public void Denormalize(DynamoDBContext context) { // analyze all PropertyStorage configs and denormalize data into other properties // all data must exist in PropertyStorage objects prior to denormalization foreach (var property in Properties) { // only add non-ignored properties if (property.IsIgnored) continue; property.Validate(context); AddPropertyStorage(property); string propertyName = property.PropertyName; foreach (var index in property.Indexes) { var gsi = index as PropertyStorage.GSI; if (gsi != null) AddGSIConfigs(gsi.IndexNames, propertyName, gsi.IsHashKey); var lsi = index as PropertyStorage.LSI; if (lsi != null) AddLSIConfigs(lsi.IndexNames, propertyName); } } //if (this.HashKeyPropertyNames.Count == 0) // throw new InvalidOperationException("No hash key configured for type " + TargetTypeInfo.FullName); if (this.Properties.Count == 0) throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Type {0} is unsupported, it has no supported members", TargetTypeInfo.FullName)); } private void AddPropertyStorage(PropertyStorage value) { string propertyName = value.PropertyName; string attributeName = value.AttributeName; //PropertyToPropertyStorageMapping[propertyName] = value; AddPropertyStorage(propertyName, value); if (!AttributesToGet.Contains(attributeName)) AttributesToGet.Add(attributeName); if (value.StoreAsEpoch) AttributesToStoreAsEpoch.Add(attributeName); if (value.IsLSIRangeKey || value.IsGSIKey) { List indexes; if (!AttributeToIndexesNameMapping.TryGetValue(attributeName, out indexes)) { indexes = new List(); AttributeToIndexesNameMapping[attributeName] = indexes; } foreach (var index in value.IndexNames) { if (!indexes.Contains(index)) indexes.Add(index); } } if (value.IsHashKey) HashKeyPropertyNames.Add(propertyName); if (value.IsRangeKey) RangeKeyPropertyNames.Add(propertyName); if (value.IsVersion) { if (!string.IsNullOrEmpty(VersionPropertyName)) throw new InvalidOperationException("Multiple version properties defined: " + VersionPropertyName + " and " + propertyName); VersionPropertyName = propertyName; } } private void AddLSIConfigs(List lsiIndexNames, string propertyName) { foreach (var index in lsiIndexNames) { List properties; if (!this.IndexNameToLSIRangePropertiesMapping.TryGetValue(index, out properties)) { properties = new List(); this.IndexNameToLSIRangePropertiesMapping[index] = properties; } if (!properties.Contains(propertyName, StringComparer.Ordinal)) properties.Add(propertyName); } } private void AddGSIConfigs(List gsiIndexNames, string propertyName, bool isHashKey) { foreach (var index in gsiIndexNames) { GSIConfig gsiConfig = this.GetGSIConfig(index); if (gsiConfig == null) { gsiConfig = new GSIConfig(index); this.IndexNameToGSIMapping[index] = gsiConfig; } if (isHashKey) gsiConfig.HashKeyPropertyName = propertyName; else gsiConfig.RangeKeyPropertyName = propertyName; } } // constructor public ItemStorageConfig(ITypeInfo targetTypeInfo) : base(targetTypeInfo) { AttributeToIndexesNameMapping = new Dictionary>(StringComparer.Ordinal); IndexNameToLSIRangePropertiesMapping = new Dictionary>(StringComparer.Ordinal); IndexNameToGSIMapping = new Dictionary(StringComparer.Ordinal); AttributesToGet = new List(); HashKeyPropertyNames = new List(); RangeKeyPropertyNames = new List(); AttributesToStoreAsEpoch = new HashSet(); } } /// /// Cache of ItemStorageConfig objects /// internal class ItemStorageConfigCache : IDisposable { // Cache of ItemStorageConfig objects per table and the // base, table-less ItemStorageConfig private class ConfigTableCache { public Dictionary Cache { get; private set; } public ItemStorageConfig BaseTypeConfig { get; private set; } public ConfigTableCache(ItemStorageConfig baseTypeConfig) { BaseTypeConfig = baseTypeConfig; BaseTableName = BaseTypeConfig.TableName; Cache = new Dictionary(StringComparer.Ordinal); } public string BaseTableName { get; private set; } } private Dictionary Cache; private DynamoDBContext Context; private bool disposedValue; private readonly ReaderWriterLockSlim _readerWriterLockSlim = new ReaderWriterLockSlim(); public ItemStorageConfigCache(DynamoDBContext context) { Cache = new Dictionary(); Context = context; } public ItemStorageConfig GetConfig(DynamoDBFlatConfig flatConfig, bool conversionOnly = false) { Type type = typeof(T); return GetConfig(type, flatConfig, conversionOnly); } public ItemStorageConfig GetConfig(Type type, DynamoDBFlatConfig flatConfig, bool conversionOnly = false) { ConfigTableCache tableCache = null; ItemStorageConfig config; string actualTableName = null; try { _readerWriterLockSlim.EnterReadLock(); Cache.TryGetValue(type, out tableCache); if(tableCache != null) { // If this type is only used for conversion, do not attempt to populate the config from the table if (conversionOnly) return tableCache.BaseTypeConfig; actualTableName = DynamoDBContext.GetTableName(tableCache.BaseTableName, flatConfig); if (tableCache.Cache.TryGetValue(actualTableName, out config)) { return config; } } } finally { if(_readerWriterLockSlim.IsReadLockHeld) { _readerWriterLockSlim.ExitReadLock(); } } try { _readerWriterLockSlim.EnterWriteLock(); if (tableCache == null) { // Check to see if another thread go the write lock before this thread and filled the cache. Cache.TryGetValue(type, out tableCache); if (tableCache == null) { var baseStorageConfig = CreateStorageConfig(type, actualTableName: null); tableCache = new ConfigTableCache(baseStorageConfig); Cache[type] = tableCache; } } // If this type is only used for conversion, do not attempt to populate the config from the table if (conversionOnly) return tableCache.BaseTypeConfig; if (actualTableName == null) { actualTableName = DynamoDBContext.GetTableName(tableCache.BaseTableName, flatConfig); } // Check to see if another thread go the write lock before this thread and filled the cache. if (tableCache.Cache.TryGetValue(actualTableName, out config)) { return config; } config = CreateStorageConfig(type, actualTableName); tableCache.Cache[actualTableName] = config; return config; } finally { if(_readerWriterLockSlim.IsWriteLockHeld) { _readerWriterLockSlim.ExitWriteLock(); } } } private static string GetAccurateCase(ItemStorageConfig config, string value) { return (config.LowerCamelCaseProperties ? Utils.ToLowerCamelCase(value) : value); } private ItemStorageConfig CreateStorageConfig(Type baseType, string actualTableName) { if (baseType == null) throw new ArgumentNullException("baseType"); ITypeInfo typeInfo = TypeFactory.GetTypeInfo(baseType); ItemStorageConfig config = new ItemStorageConfig(typeInfo); PopulateConfigFromType(config, typeInfo); PopulateConfigFromMappings(config, AWSConfigsDynamoDB.Context.TypeMappings); // try to populate config from table definition only if actual table name is known if (!string.IsNullOrEmpty(actualTableName)) { Table table; try { table = Context.GetUnconfiguredTable(actualTableName); } catch { table = null; } if (table != null) { PopulateConfigFromTable(config, table); } } config.Denormalize(Context); return config; } private static void PopulateConfigFromType(ItemStorageConfig config, ITypeInfo typeInfo) { DynamoDBTableAttribute tableAttribute = Utils.GetTableAttribute(typeInfo); if (tableAttribute == null) { config.TableName = typeInfo.Name; } else { if (string.IsNullOrEmpty(tableAttribute.TableName)) throw new InvalidOperationException("DynamoDBTableAttribute.Table is empty or null"); config.TableName = tableAttribute.TableName; config.LowerCamelCaseProperties = tableAttribute.LowerCamelCaseProperties; } string tableAlias; if (AWSConfigsDynamoDB.Context.TableAliases.TryGetValue(config.TableName, out tableAlias)) config.TableName = tableAlias; foreach (var member in typeInfo.GetMembers()) { if (!StorageConfig.IsValidMemberInfo(member)) continue; // prepare basic info PropertyStorage propertyStorage = new PropertyStorage(member); propertyStorage.AttributeName = GetAccurateCase(config, member.Name); // run through all DDB attributes List allAttributes = Utils.GetAttributes(member); foreach (var attribute in allAttributes) { // filter out ignored properties if (attribute is DynamoDBIgnoreAttribute) propertyStorage.IsIgnored = true; if (attribute is DynamoDBVersionAttribute) propertyStorage.IsVersion = true; DynamoDBRenamableAttribute renamableAttribute = attribute as DynamoDBRenamableAttribute; if (renamableAttribute != null && !string.IsNullOrEmpty(renamableAttribute.AttributeName)) { propertyStorage.AttributeName = GetAccurateCase(config, renamableAttribute.AttributeName); } DynamoDBPropertyAttribute propertyAttribute = attribute as DynamoDBPropertyAttribute; if (propertyAttribute != null) { propertyStorage.StoreAsEpoch = propertyAttribute.StoreAsEpoch; if (propertyAttribute.Converter != null) propertyStorage.ConverterType = propertyAttribute.Converter; if (propertyAttribute is DynamoDBHashKeyAttribute) { var gsiHashAttribute = propertyAttribute as DynamoDBGlobalSecondaryIndexHashKeyAttribute; if (gsiHashAttribute != null) { propertyStorage.IsGSIHashKey = true; propertyStorage.AddIndex(gsiHashAttribute); } else propertyStorage.IsHashKey = true; } if (propertyAttribute is DynamoDBRangeKeyAttribute) { var gsiRangeAttribute = propertyAttribute as DynamoDBGlobalSecondaryIndexRangeKeyAttribute; if (gsiRangeAttribute != null) { propertyStorage.IsGSIRangeKey = true; propertyStorage.AddIndex(gsiRangeAttribute); } else propertyStorage.IsRangeKey = true; } DynamoDBLocalSecondaryIndexRangeKeyAttribute lsiRangeKeyAttribute = propertyAttribute as DynamoDBLocalSecondaryIndexRangeKeyAttribute; if (lsiRangeKeyAttribute != null) { propertyStorage.IsLSIRangeKey = true; propertyStorage.AddIndex(lsiRangeKeyAttribute); } } } config.Properties.Add(propertyStorage); } } private static void PopulateConfigFromTable(ItemStorageConfig config, Table table) { PropertyStorage property; // keys foreach(var key in table.Keys) { var attributeName = key.Key; var keyDescription = key.Value; property = GetProperty(config, attributeName, false); // validate against table if (property.IsKey) ValidateProperty(property.IsHashKey == keyDescription.IsHash, property.PropertyName, "Property key definition must match table key definition"); // populate property if (keyDescription.IsHash) property.IsHashKey = true; else property.IsRangeKey = true; } foreach (var kvp in table.GlobalSecondaryIndexes) { string indexName = kvp.Key; var gsi = kvp.Value; foreach (var element in gsi.KeySchema) { string attributeName = element.AttributeName; bool isHashKey = element.KeyType == KeyType.HASH; property = GetProperty(config, attributeName, true); if (property != null) { property.AddGsiIndex(isHashKey, attributeName, indexName); } } } foreach (var kvp in table.LocalSecondaryIndexes) { string indexName = kvp.Key; var lsi = kvp.Value; foreach (var element in lsi.KeySchema) { string attributeName = element.AttributeName; bool isHashKey = element.KeyType == KeyType.HASH; // only add for range keys if (!isHashKey) { property = GetProperty(config, attributeName, true); if (property != null) { property.AddLsiIndex(attributeName, indexName); } } } } } private static void PopulateConfigFromMappings(ItemStorageConfig config, Dictionary typeMappings) { var baseType = config.TargetTypeInfo.Type; TypeMapping typeMapping; if (typeMappings.TryGetValue(baseType, out typeMapping)) { config.TableName = typeMapping.TargetTable; if (AWSConfigsDynamoDB.Context.TableAliases.TryGetValue(config.TableName, out var tableAlias)) config.TableName = tableAlias; foreach (var kvp in typeMapping.PropertyConfigs) { PropertyConfig propertyConfig = kvp.Value; string propertyName = propertyConfig.Name; PropertyStorage propertyStorage; if (!config.FindPropertyByPropertyName(propertyName, out propertyStorage)) throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "No matching property {0} on type {1}", propertyName, baseType.FullName)); if (!string.IsNullOrEmpty(propertyConfig.Attribute)) propertyStorage.AttributeName = propertyConfig.Attribute; if (propertyConfig.Converter != null) propertyStorage.ConverterType = propertyConfig.Converter; propertyStorage.IsIgnored = propertyConfig.Ignore; propertyStorage.IsVersion = propertyConfig.Version; propertyStorage.StoreAsEpoch = propertyConfig.StoreAsEpoch; } } } // Finds an existing PropertyStorage for a property by its attributeName, // or creates a new PropertyStorage and adds it to the config. // If a property is non-optional and is not present on the type, throws an exception. // If a property is optional and not present on the type, returns null. private static PropertyStorage GetProperty(ItemStorageConfig config, string attributeName, bool optional) { PropertyStorage property = null; bool exists = config.FindSinglePropertyByAttributeName(attributeName, out property); if (!exists) { // property storage doesn't exist yet, create and populate MemberInfo member; // for optional properties/attributes, a null MemberInfo is OK Validate(config.TargetTypeMembers.TryGetValue(attributeName, out member) || optional, "Unable to locate property for key attribute {0}", attributeName); if (member != null) { property = new PropertyStorage(member); property.AttributeName = attributeName; config.Properties.Add(property); } } return property; } private static void Validate(bool value, string messageFormat, params object[] args) { if (!value) { StringBuilder sb = new StringBuilder(); sb.AppendFormat(CultureInfo.InvariantCulture, messageFormat, args); throw new InvalidOperationException(sb.ToString()); } } private static void ValidateProperty(bool value, string propertyName, string messageFormat, params object[] args) { if (!value) { StringBuilder sb = new StringBuilder(); sb.AppendFormat(CultureInfo.InvariantCulture, "Error with property [{0}]: ", propertyName); sb.AppendFormat(CultureInfo.InvariantCulture, messageFormat, args); throw new InvalidOperationException(sb.ToString()); } } protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { _readerWriterLockSlim.Dispose(); } disposedValue = true; } } public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } } }