/*
* 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 Amazon.DynamoDBv2.DocumentModel;
using Amazon.DynamoDBv2.Model;
using Amazon.Runtime;
using Amazon.Runtime.Internal.Util;
using Amazon.Util;
using Amazon.Util.Internal;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using ThirdParty.RuntimeBackports;
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
[DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)]
public Type MemberType { get; protected set; }
// Converter type, if one is present
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)]
public Type ConverterType { get; set; }
// Converter, if one is present
public IPropertyConverter Converter { get; protected set; }
public void AddDerivedType(string typeDiscriminator, [DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type type)
{
_derivedTypesDictionary[type] = typeDiscriminator;
_derivedTypeKeysDictionary[typeDiscriminator] = type;
}
// derived type information used for polymorphic serialization
private Dictionary _derivedTypesDictionary;
// derived type information used for polymorphic deserialization
private Dictionary _derivedTypeKeysDictionary;
public bool TryGetDerivedTypeDiscriminator([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type type, out string typeDiscriminator)
{
if (_derivedTypesDictionary.TryGetValue(type, out typeDiscriminator))
{
return true;
}
return false;
}
#if NET8_0_OR_GREATER
///
/// The types the deserializer can produce for this member in addition to MemberType , that is the
/// derived types registered for polymorphic deserialization.
///
internal IEnumerable PolymorphicDerivedTypes
{
get { return _derivedTypeKeysDictionary.Values; }
}
#endif
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2067",
Justification = "The user's type has been annotated with InternalConstants.DataModelModeledType with the public API into the library. At this point the type will not be trimmed.")]
public bool TryGetDerivedType(string typeDiscriminator, [DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] out Type deriviedType)
{
if (_derivedTypeKeysDictionary.TryGetValue(typeDiscriminator, out deriviedType))
{
return true;
}
return false;
}
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072",
Justification = "The user's type has been annotated with DynamicallyAccessedMemberTypes.All with the public API into the library. At this point the type will not be trimmed.")]
internal SimplePropertyStorage(MemberInfo member)
: this(Utils.GetType(member))
{
Member = member;
PropertyName = member.Name;
}
internal SimplePropertyStorage([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type memberType)
{
MemberType = memberType;
_derivedTypesDictionary = new Dictionary();
_derivedTypeKeysDictionary = new Dictionary();
}
internal SimplePropertyStorage([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type memberType, SimplePropertyStorage parentProeprtyStorage)
{
MemberType = memberType;
_derivedTypesDictionary = parentProeprtyStorage._derivedTypesDictionary;
_derivedTypeKeysDictionary = parentProeprtyStorage._derivedTypeKeysDictionary;
}
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 this member's value is supplied through a parameterized (binding) constructor rather
/// than being set after construction. When true , the value is passed as a constructor argument
/// during deserialization and is not written via a property/field setter. Only used on the .NET 8+ target.
///
public bool IsConstructorArgument { get; set; }
///
/// Whether to store DateTime as epoch seconds integer.
///
public bool StoreAsEpoch { get; set; }
///
/// Whether to store DateTime as epoch seconds integer (with support for dates AFTER 2038).
///
public bool StoreAsEpochLong { get; set; }
///
/// Whether to store Type Discriminator for polymorphic serialization.
///
public bool PolymorphicProperty { get; set; }
///
/// Whether to store child properties at the same level as the parent property.
///
public bool ShouldFlattenChildProperties { get; set; }
///
/// For a member, the effective
/// the flattened child type is serialized with on save (its own declared casing, or the casing the
/// enclosing type propagates to an undecorated child). Load must resolve any non-flattened complex
/// leaf of the flattened child with this same casing so nested Map keys round-trip.
/// This is also stamped onto each flattened leaf (see ) so that a
/// condition (ScanCondition/QueryCondition) targeting a flattened complex leaf serializes its
/// value's Map keys with the flattened child's casing rather than the root's.
///
public CaseMode FlattenedEffectiveCasing { get; set; }
///
/// Whether to store property at parent level.
///
public bool IsFlattened { get; set; }
///
/// Whether to store the property as a timestamp that is automatically generated.
///
public bool IsAutoGeneratedTimestamp { get; set; }
///
/// Behavior for handling updates to this property during a save operation.
///
public UpdateBehavior UpdateBehaviorMode { get; set; }
// corresponding IndexNames, if applicable
public List IndexNames { get; set; }
public List FlattenProperties { get; set; }
public bool IsCounter { get; set; }
public long CounterDelta { get; set; }
public long CounterStartValue { get; set; }
///
/// Whether this property is a search vector associated with one or more vector indexes.
///
public bool IsSearchVector { get; set; }
///
/// Names of the vector indexes this search vector belongs to.
///
public List SearchVectorIndexNames { get; set; }
public void AddIndex(DynamoDBGlobalSecondaryIndexHashKeyAttribute gsiHashKey)
{
AddIndex(new GSI(true, gsiHashKey.AttributeName, gsiHashKey.Order, gsiHashKey.IndexNames));
}
public void AddIndex(DynamoDBGlobalSecondaryIndexRangeKeyAttribute gsiRangeKey)
{
AddIndex(new GSI(false, gsiRangeKey.AttributeName, gsiRangeKey.Order, gsiRangeKey.IndexNames));
}
public void AddIndex(DynamoDBLocalSecondaryIndexRangeKeyAttribute lsiRangeKey)
{
AddIndex(new LSI(lsiRangeKey.AttributeName, lsiRangeKey.IndexNames));
}
public void AddGsiIndex(bool isHashKey, string attributeName,int order, params string[] indexNames)
{
AddIndex(new GSI(isHashKey, attributeName, order, 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 int Order { get; private set; }
public GSI(bool isHashKey, string attributeName, int order, params string[] indexNames)
: base(attributeName, indexNames)
{
IsHashKey = isHashKey;
Order = order;
}
}
///
/// Validates configurations and sets required fields
///
public void Validate(DynamoDBContext context)
{
if (IsVersion)
Utils.ValidateNumericType(MemberType); // no conversion is possible, so type must be a nullable primitive
if (IsCounter)
Utils.ValidateNumericType(MemberType); // no conversion is possible, so type must be a nullable primitive
if (IsSearchVector)
{
Utils.ValidateSearchVectorType(MemberType);
if (context?.Config?.Conversion == DynamoDBEntryConversion.V1)
throw new InvalidOperationException(
"Property " + PropertyName + " is a search vector, which is not supported with DynamoDBEntryConversion.V1. Use DynamoDBEntryConversion.V2 instead.");
}
if (IsHashKey && IsRangeKey)
throw new InvalidOperationException("Property " + PropertyName + " cannot be both hash and range key");
if (UpdateBehaviorMode == UpdateBehavior.IfNotExists && (IsKey || IsVersion || IsCounter))
{
string propertyType = IsKey ? "key" : IsVersion ? "version" : IsCounter ? "counter" : "unknown";
throw new InvalidOperationException($"Property {PropertyName} cannot be a {propertyType} and have UpdateBehavior set to IfNotExists at the same time.");
}
if (ConverterType != null)
{
if (PolymorphicProperty)
throw new InvalidOperationException("Converter for " + PropertyName + " must not be set at the same time as derived types.");
if (ShouldFlattenChildProperties)
throw new InvalidOperationException("Converter for " + PropertyName + " must not be set at the same time as flatten types.");
if (StoreAsEpoch || StoreAsEpochLong)
throw new InvalidOperationException("Converter for " + PropertyName + " must not be set at the same time as StoreAsEpoch or StoreAsEpochLong is set to true");
if (IsAutoGeneratedTimestamp)
throw new InvalidOperationException("Converter for " + PropertyName + " must not be set at the same time as AutoGeneratedTimestamp 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;
}
else
{
if (context.ConverterCache.TryGetValue(MemberType, out IPropertyConverter converter) && converter != null)
{
if (!PolymorphicProperty && !ShouldFlattenChildProperties && !StoreAsEpoch && !StoreAsEpochLong && !IsAutoGeneratedTimestamp)
this.Converter = converter;
}
}
if (StoreAsEpoch && StoreAsEpochLong)
throw new InvalidOperationException(PropertyName + " must not set both StoreAsEpoch and StoreAsEpochLong as true at the same time.");
if (IsAutoGeneratedTimestamp)
Utils.ValidateTimestampType(MemberType);
foreach (var index in Indexes)
IndexNames.AddRange(index.IndexNames);
}
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026",
Justification = "The user's type has been annotated with DynamicallyAccessedMemberTypes.All with the public API into the library. At this point the type will not be trimmed.")]
internal PropertyStorage(MemberInfo member)
: base(member)
{
IndexNames = new List();
Indexes = new List();
FlattenProperties = new List();
SearchVectorIndexNames = new List();
UpdateBehaviorMode = UpdateBehavior.Always;
}
}
///
/// 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;
HashKeyPropertyNames = new Dictionary();
RangeKeyPropertyNames = new Dictionary();
}
// index name
public string IndexName { get; set; }
// keys
public Dictionary HashKeyPropertyNames { get; set; }
public Dictionary RangeKeyPropertyNames { get; set; }
}
///
/// Storage information for a specific class
///
internal class StorageConfig
{
// normalized PropertyStorage objects
public List Properties { get; private set; }
// target type
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
public Type TargetType { get; private set; }
// target type members
public Dictionary TargetTypeMembers { get; private set; }
#if NET8_0_OR_GREATER
///
/// Pairs a constructor parameter with the that supplies its value.
///
internal readonly struct ConstructorArgument
{
public ConstructorArgument(ParameterInfo parameter, PropertyStorage storage)
{
Parameter = parameter;
Storage = storage;
}
public ParameterInfo Parameter { get; }
public PropertyStorage Storage { get; }
}
///
/// The constructor used to populate instances of by binding stored attribute
/// values to its parameters, or null when the type is populated via the parameterless path.
///
internal ConstructorInfo BindingConstructor { get; private set; }
///
/// The binding-constructor parameters aligned with the that supplies each value.
/// Populated by .
///
internal ConstructorArgument[] ConstructorArguments { get; private set; }
///
/// The names of the binding constructor's parameters, used to surface get-only members during discovery.
///
internal string[] ConstructorParameterNames { get; private set; }
#endif
// storage mappings
private Dictionary PropertyToPropertyStorageMapping { get; set; }
internal void AddPropertyStorage(string propertyName, PropertyStorage propertyStorage)
{
// Check for existing property with the same attribute name
foreach (var existing in PropertyToPropertyStorageMapping.Values)
{
if (string.Equals(existing.AttributeName, propertyStorage.AttributeName))
{
throw new InvalidOperationException(
$"A property with attribute name '{propertyStorage.AttributeName}' already exists (property: '{existing.PropertyName}'). " +
$"Cannot add property '{propertyName}' with the same attribute name.");
}
}
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);
}
private static Dictionary GetMembersDictionary([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type type, ICollection constructorParameterNames)
{
Dictionary dictionary = new Dictionary(StringComparer.Ordinal);
var members = Utils.GetMembersFromType(type, constructorParameterNames);
foreach (var member in members)
{
InternalSDKUtils.AddToDictionary(dictionary, member.Name, member);
}
return dictionary;
}
// constructor
internal StorageConfig([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type targetType)
{
ICollection constructorParameterNames = null;
bool requiresParameterlessConstructor = true;
#if NET8_0_OR_GREATER
// Immutable types (e.g. record / record struct) are populated by binding stored values to a
// parameterized constructor rather than instantiating with a parameterless constructor.
if (Utils.TryGetBindingConstructor(targetType, out var bindingConstructor))
{
BindingConstructor = bindingConstructor;
ConstructorParameterNames = bindingConstructor.GetParameters().Select(p => p.Name).ToArray();
constructorParameterNames = ConstructorParameterNames;
requiresParameterlessConstructor = false;
}
else if (targetType.IsValueType && !targetType.ContainsGenericParameters && !targetType.IsByRefLike)
{
// Value types (e.g. a non-positional record struct) that have no binding constructor are
// populated via zero-initialization (default(T)) followed by member assignment, so they do
// not require a parameterless constructor (CanInstantiate only accepts reference types).
// Open generic and byref-like value types cannot be boxed, so they must not take this path;
// leaving them to CanInstantiate keeps the normal unsupported-type error authoritative
// instead of failing later inside Array.CreateInstance.
requiresParameterlessConstructor = false;
}
#endif
if (requiresParameterlessConstructor && !Utils.CanInstantiate(targetType))
{
string errorMessage;
if (InternalSDKUtils.IsRunningNativeAot())
{
errorMessage = $"Type {targetType.FullName} is unsupported, it cannot be instantiated. Since the application is running in Native AOT mode the type could possibly be trimmed. " +
"This can happen if the type being created is a nested type of a type being used for saving and loading DynamoDB items. " +
$"This can be worked around by adding the \"[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof({targetType.FullName}))]\" attribute to the constructor of the parent type." +
"If the parent type can not be modified the attribute can also be used on the method invoking the DynamoDB sdk or some other method that you are sure is not being trimmed.";
}
else
{
errorMessage = $"Type {targetType.FullName} is unsupported, it cannot be instantiated.";
}
#if !NET8_0_OR_GREATER
// Populating a type through a parameterized constructor (records, other immutable types, and
// value types) is only available on the .NET 8 or later build of the SDK. Point users at that
// requirement instead of leaving them with a bare "cannot be instantiated" message.
errorMessage += " Retargeting to .NET 8 (or later) can enable records and other types with a usable public parameterized constructor (selected with [DynamoDBConstructor] when necessary); otherwise, expose a public parameterless constructor and settable members.";
#endif
throw new InvalidOperationException(errorMessage);
}
TargetType = targetType;
Properties = new List();
PropertyToPropertyStorageMapping = new Dictionary(StringComparer.Ordinal);
TargetTypeMembers = GetMembersDictionary(targetType, constructorParameterNames);
if (TargetTypeMembers.Count == 0)
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
"Type {0} is unsupported, it has no supported members", targetType.FullName));
}
#if NET8_0_OR_GREATER
///
/// Aligns the binding constructor's parameters with the that supplies
/// each value (matched by property name, case-insensitive) and flags those members as constructor
/// arguments, then validates that every modeled member can actually be populated when an item is loaded.
/// Must be called after has been populated and after type mappings and table
/// configuration have been applied.
///
internal void ResolveConstructorArguments()
{
if (BindingConstructor != null)
{
var parameters = BindingConstructor.GetParameters();
var arguments = new ConstructorArgument[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
PropertyStorage match = FindConstructorArgumentMember(parameter);
if (match == null)
{
// The parameter is supplied only by members that are not persisted, so there is never a
// stored value for it. Bind it with no storage and let it take its default on every load.
arguments[i] = new ConstructorArgument(parameter, null);
continue;
}
// Reject members whose value the SDK reconciles onto the instance after a save/update, since that
// write-back is impossible for an immutable constructor-populated member. This includes members
// nested inside a flattened constructor argument, whose server-managed descendants would otherwise
// be silently left stale (the flattened parent is skipped by PopulateInstance).
ValidateConstructorBindableMember(match, viaFlatten: false);
ValidateConstructorArgumentType(parameter, match);
match.IsConstructorArgument = true;
arguments[i] = new ConstructorArgument(parameter, match);
}
ConstructorArguments = arguments;
// Every remaining member is set through a setter when an item is loaded, so a read-only member that
// was not bound to a constructor parameter could be saved but never loaded.
ValidateAllMembersAreLoadable();
}
foreach (var property in Properties)
{
if (property.IsIgnored) continue;
if (TargetType.IsValueType)
{
// A value type is boxed before the SDK reconciles server-produced values onto the instance, so the
// write-back updates a copy and the caller's value silently keeps the stale value.
ValidateValueTypeMember(property, viaFlatten: false, TargetType.FullName);
}
else
{
// A reference type can still own a flattened value-type member. Its children are enumerated into
// this configuration directly, so nothing here would reject a server-managed descendant and the
// item would load, while saving reaches the value type's own configuration and fails. Checking it
// here makes load and save reject the model consistently.
ValidateFlattenedValueTypeMembers(property);
}
}
}
///
/// Applies the value-type rule to any flattened member whose type is a value type, at any depth. Such a
/// member is materialized by zero-initialization and assigned back to its parent when an item is loaded,
/// so the owning configuration accepts it even when the owner is a reference type.
///
private void ValidateFlattenedValueTypeMembers(PropertyStorage member)
{
if (!member.ShouldFlattenChildProperties || member.FlattenProperties == null)
return;
if (member.MemberType != null && member.MemberType.IsValueType)
{
// This validates the member's whole flattened subtree, so there is nothing further to walk.
ValidateValueTypeMember(member, viaFlatten: false, member.MemberType.FullName);
return;
}
foreach (var child in member.FlattenProperties)
{
if (child.IsIgnored) continue;
ValidateFlattenedValueTypeMembers(child);
}
}
///
/// Finds the single modeled member that supplies , or null when the
/// parameter is only matched by members that are not persisted. Members are matched to constructor
/// parameters by name, case-insensitively; when more than one member matches, an exact (case-sensitive)
/// match wins, and anything else is rejected as ambiguous rather than guessed.
///
private PropertyStorage FindConstructorArgumentMember(ParameterInfo parameter)
{
var named = Properties
.Where(ps => string.Equals(ps.PropertyName, parameter.Name, StringComparison.OrdinalIgnoreCase))
.ToList();
var candidates = named.Where(ps => !ps.IsIgnored).ToList();
if (candidates.Count == 0)
{
// A member exists but is marked [DynamoDBIgnore], so the parameter simply has no stored value.
if (named.Count > 0)
return null;
throw new InvalidOperationException(
$"Constructor parameter '{parameter.Name}' of type {TargetType.FullName} does not map to a modeled member. " +
"Every binding constructor parameter must correspond to a member of the same name (matched case-insensitively): " +
"a property, or a public field that is not readonly. Alternatively the member may be marked with " +
"[DynamoDBIgnore], in which case the parameter always receives its default value.");
}
if (candidates.Count == 1)
return candidates[0];
var exactMatches = candidates
.Where(ps => string.Equals(ps.PropertyName, parameter.Name, StringComparison.Ordinal))
.ToList();
if (exactMatches.Count == 1)
return exactMatches[0];
throw new InvalidOperationException(
$"Constructor parameter '{parameter.Name}' of type {TargetType.FullName} matches more than one modeled member " +
$"({string.Join(", ", candidates.Select(ps => "'" + ps.PropertyName + "'"))}). Constructor parameters are matched to members " +
"by name, case-insensitively, so this mapping is ambiguous. Rename the parameter to exactly match the member that supplies it, " +
"or mark the members that should not be persisted with [DynamoDBIgnore].");
}
///
/// Throws when a modeled member cannot be populated while loading an item because it is neither supplied
/// through the binding constructor nor writable. Such a member is still written when the item is saved, so
/// allowing it would produce stored items that fail to load.
///
private void ValidateAllMembersAreLoadable()
{
foreach (var property in Properties)
{
if (property.IsIgnored || property.IsConstructorArgument) continue;
if (Utils.IsReadWrite(property.Member)) continue;
throw new InvalidOperationException(
$"Property '{property.PropertyName}' of type {TargetType.FullName} is read-only and is not supplied through the constructor " +
"used for DynamoDB deserialization, so it would be written when the item is saved but could never be populated when it is loaded. " +
"Mark it with [DynamoDBIgnore], make it settable, or select a constructor that includes it with [DynamoDBConstructor].");
}
}
///
/// Throws when a constructor parameter's type cannot accept the value produced for the member that supplies
/// it. A stored attribute is deserialized as its member's type and then handed to
/// , so an incompatible pair (for example an int
/// parameter fed by a long member) saves successfully and then fails on every load.
///
private void ValidateConstructorArgumentType(ParameterInfo parameter, PropertyStorage member)
{
// A converter decides the run-time type of the deserialized value, so the member's declared type says
// nothing useful about what the constructor will receive. Converter is the effective converter: it
// covers an attribute or type-mapping converter and a default converter registered on the context,
// which is why this runs after Denormalize has called PropertyStorage.Validate.
if (member.Converter != null || member.ConverterType != null)
return;
// The loader can produce the member's declared type, any derived type declared on the member itself,
// and any declared on the member's type with [DynamoDBPolymorphicType].
var runtimeTypes = member.PolymorphicDerivedTypes
.Concat(Utils.GetPolymorphicDerivedTypes(member.MemberType));
if (Utils.IsAssignableToConstructorParameter(member.MemberType, parameter.ParameterType, runtimeTypes))
return;
throw new InvalidOperationException(
$"Constructor parameter '{parameter.Name}' of type {TargetType.FullName} is declared as " +
$"{parameter.ParameterType.FullName}, but the member '{member.PropertyName}' that supplies it is " +
$"{member.MemberType.FullName}. A stored attribute is deserialized as its member's type and then passed to the " +
"constructor, so this combination would save successfully but fail to load with an argument-type error. " +
"Declare the parameter and the member with the same type. The constructor is called through reflection, which " +
"accepts only an identical type, a nullable and its underlying type in either direction, a reference conversion " +
"to a base type or interface, an enum and its underlying type, and a widening numeric conversion. It does not " +
"perform every C# implicit conversion: int to decimal, for example, is rejected because decimal is not a " +
"primitive type. A parameter type derived from the member's type is only supported when a " +
"[DynamoDBPolymorphicType] mapping lets the loader create that derived type.");
}
///
/// Returns a description of why holds a value that the SDK reconciles onto the
/// instance after a save or update, or null when it holds no such value. The SDK writes a
/// server-produced value back onto the instance for these members once the request completes.
///
private static string GetServerManagedMemberKind(PropertyStorage member)
{
if (member.IsVersion) return "a version property";
if (member.IsCounter) return "an atomic counter property";
if (member.IsAutoGeneratedTimestamp) return "an auto-generated timestamp property";
if (member.UpdateBehaviorMode == UpdateBehavior.IfNotExists) return "a property that uses UpdateBehavior.IfNotExists";
return null;
}
///
/// Throws if (or, when it is flattened, any of its descendants) holds a value that
/// the SDK reconciles onto the instance after a save/update — a version, atomic counter, auto-generated
/// timestamp, or member — because that write-back cannot be applied
/// to an immutable member supplied through the constructor.
///
private void ValidateConstructorBindableMember(PropertyStorage member, bool viaFlatten)
{
var kind = GetServerManagedMemberKind(member);
if (kind != null)
{
string location = viaFlatten
? $"reached through a flattened constructor parameter of type {TargetType.FullName}"
: $"of type {TargetType.FullName}";
throw new InvalidOperationException(
$"Property '{member.PropertyName}' ({location}) is {kind} and cannot be supplied through a constructor parameter. " +
"These properties are updated by the SDK after a save or update, which requires writing the new value back onto the instance; " +
"that is not possible for an immutable (constructor-populated) member. Make it a settable property on a type with a parameterless constructor instead.");
}
if (member.ShouldFlattenChildProperties && member.FlattenProperties != null)
{
foreach (var child in member.FlattenProperties)
{
// Denormalize excludes ignored flattened descendants, so they are neither persisted nor
// written back and cannot go stale.
if (child.IsIgnored) continue;
ValidateConstructorBindableMember(child, viaFlatten: true);
}
}
}
///
/// Throws if (or, when it is flattened, any of its descendants) holds a value that
/// the SDK reconciles onto the instance after a save/update and the declaring type is a value type. The SDK
/// boxes the instance before assigning those values through reflection, so the assignment updates the box
/// rather than the caller's value: optimistic locking would silently break and atomic counter and
/// UpdateBehavior.IfNotExists members would silently read a stale value after a save.
///
/// The member to validate, together with its flattened descendants.
/// Whether was reached through a flattened member.
///
/// The value type the rule is applied on behalf of: the configured type itself when that type is a value
/// type, or a flattened value-type member's type when the owner is a reference type.
///
private void ValidateValueTypeMember(PropertyStorage member, bool viaFlatten, string valueTypeName)
{
var kind = GetServerManagedMemberKind(member);
if (kind != null)
{
string location = viaFlatten
? $"reached through a flattened member of value type {valueTypeName}"
: $"of value type {valueTypeName}";
throw new InvalidOperationException(
$"Property '{member.PropertyName}' ({location}) is {kind} and cannot be declared on a value type (struct or record struct). " +
"The SDK writes the new value back onto the instance after a save or update, but a value type is boxed before that assignment, " +
"so the caller's value would silently keep the stale value. Declare the type as a class instead.");
}
if (member.ShouldFlattenChildProperties && member.FlattenProperties != null)
{
foreach (var child in member.FlattenProperties)
{
// Denormalize excludes ignored flattened descendants, so they are neither persisted nor
// written back and cannot go stale.
if (child.IsIgnored) continue;
ValidateValueTypeMember(child, viaFlatten: true, valueTypeName);
}
}
}
#endif
}
///
/// Storage information for a specific class that is associated with a table
///
internal class ItemStorageConfig
{
// table
public string TableName { get; set; }
public StorageConfig BaseTypeStorageConfig { get; private set; }
public Dictionary PolymorphicTypesStorageConfig { get; private set; }
public Dictionary PolymorphicConfig { get; private set; }
///
/// The effective casing mode for this type, resolved from the type's
/// (or inherited from an enclosing type when this type does not declare its own). This is always a
/// concrete mode by the time it is on the config; use to tell whether
/// the value came from an explicit declaration on the type.
///
public CaseMode AttributeCasing { get; set; }
///
/// True when this type explicitly declared its casing (via a non-null
/// or the obsolete
/// flag). A nested type that declares
/// its own casing is never overridden by an enclosing type's casing, so an explicit
/// correctly blocks inheritance of an enclosing
/// .
///
public bool DeclaresOwnCasing { get; set; }
public HashSet AttributesToStoreAsEpoch { get; set; }
public HashSet AttributesToStoreAsEpochLong { get; set; }
// keys
public List HashKeyPropertyNames { get; private set; }
public List RangeKeyPropertyNames { get; private set; }
// properties to get
public List AttributesToGet { get; private set; }
public Expression ProjectionExpression { get; private set; }
// version
public string VersionPropertyName { get; private set; }
public bool HasVersion { get { return !string.IsNullOrEmpty(VersionPropertyName); } }
public bool HasAutogeneratedProperties { get; internal set; }
// 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; }
// search vector indexName to property name mapping (declared via DynamoDBSearchVectorAttribute)
public Dictionary IndexNameToSearchVectorPropertiesMapping { get; set; }
// entity conversion
public DynamoDBEntryConversion Conversion { get; set; }
public bool StorePolymorphicTypes => this.PolymorphicTypesStorageConfig.Any();
//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 List GetCorrectHashKeyProperty(DynamoDBFlatConfig currentConfig, List hashKeyProperties)
{
if (currentConfig.IsIndexOperation)
{
string indexName = currentConfig.IndexName;
GSIConfig gsiConfig = this.GetGSIConfig(indexName);
// Use GSI hash key if GSI is found AND GSI hash-keys are set
if (gsiConfig != null && gsiConfig.HashKeyPropertyNames != null && gsiConfig.HashKeyPropertyNames.Count != 0)
hashKeyProperties = gsiConfig.HashKeyPropertyNames.OrderBy(kv => kv.Value).Select(kv => kv.Key).ToList(); ;
}
return hashKeyProperties;
}
public List GetRangeKeyByIndex(string indexName)
{
List rangeKeyPropertyNames = null;
// test LSI first
List rangeProperties;
if (IndexNameToLSIRangePropertiesMapping.TryGetValue(indexName, out rangeProperties) &&
rangeProperties != null && rangeProperties.Count == 1)
{
rangeKeyPropertyNames = rangeProperties;
}
GSIConfig gsiConfig = GetGSIConfig(indexName);
if (gsiConfig != null)
{
rangeKeyPropertyNames = gsiConfig.RangeKeyPropertyNames.OrderBy(kv => kv.Value)
.Select(kv=> kv.Key).ToList();
}
return rangeKeyPropertyNames;
}
public PropertyStorage VersionPropertyStorage
{
get
{
if (!HasVersion) throw new InvalidOperationException("No version field defined for this type");
return this.BaseTypeStorageConfig.GetPropertyStorage(VersionPropertyName);
}
}
public void Denormalize(DynamoDBContext context, string derivedTypeAttributeName)
{
// analyze all PropertyStorage configs and denormalize data into other properties
// all data must exist in PropertyStorage objects prior to denormalization
foreach (var property in this.BaseTypeStorageConfig.Properties)
{
ProcessProperty(property, this.BaseTypeStorageConfig, true);
}
foreach (var polymorphicTypesProperty in this.PolymorphicTypesStorageConfig)
{
foreach (var polymorphicProperty in polymorphicTypesProperty.Value.Properties)
{
ProcessProperty(polymorphicProperty, polymorphicTypesProperty.Value, false);
}
}
if (StorePolymorphicTypes)
{
AttributesToGet.Add(derivedTypeAttributeName);
AddAttributeNameToProjectionExpression(derivedTypeAttributeName);
}
if (this.BaseTypeStorageConfig.Properties.Count == 0)
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
"Type {0} is unsupported, it has no supported members", this.BaseTypeStorageConfig.TargetType.FullName));
return;
void ProcessProperty(PropertyStorage property, StorageConfig storageConfig, bool setKeyProperties)
{
// only add non-ignored properties
if (property.IsIgnored)
return;
property.Validate(context);
SetPropertyConfig(property, storageConfig, setKeyProperties);
if (!property.ShouldFlattenChildProperties) return;
// flatten properties
foreach (var flattenProperty in property.FlattenProperties)
{
ProcessProperty(flattenProperty, storageConfig, setKeyProperties);
}
}
void SetPropertyConfig(PropertyStorage property, StorageConfig storageConfig, bool setKeyProperties)
{
AddPropertyStorage(property, storageConfig);
string propertyName = property.PropertyName;
if (setKeyProperties)
{
AddKeyPropertyNames(property, propertyName);
}
foreach (var index in property.Indexes)
{
var gsi = index as PropertyStorage.GSI;
if (gsi != null)
AddGSIConfigs(gsi.IndexNames, propertyName, gsi.IsHashKey, gsi.Order);
var lsi = index as PropertyStorage.LSI;
if (lsi != null)
AddLSIConfigs(lsi.IndexNames, propertyName);
}
if (property.IsSearchVector)
AddSearchVectorConfigs(property.SearchVectorIndexNames, propertyName);
}
}
public void AddPolymorphicPropertyStorageConfiguration(string typeDiscriminator, Type derivedType, StorageConfig polymorphicStorageConfig)
{
this.PolymorphicTypesStorageConfig.Add(typeDiscriminator, polymorphicStorageConfig);
this.PolymorphicConfig.Add(derivedType, typeDiscriminator);
}
private void AddAttributeNameToProjectionExpression(string derivedTypeAttributeName)
{
var expressionAttributeCount = ProjectionExpression.ExpressionAttributeNames.Count;
var expressionAttributeName = "#P" + expressionAttributeCount.ToString(CultureInfo.InvariantCulture);
var currentExpressionStatement = ProjectionExpression.ExpressionStatement;
if (string.IsNullOrEmpty(currentExpressionStatement))
{
ProjectionExpression.ExpressionStatement = expressionAttributeName;
}
else
{
ProjectionExpression.ExpressionStatement = expressionAttributeCount > 0
? string.Concat(currentExpressionStatement, ", ", expressionAttributeName)
: string.Concat(currentExpressionStatement, expressionAttributeName);
}
ProjectionExpression.ExpressionAttributeNames.Add(expressionAttributeName, derivedTypeAttributeName);
}
private void AddPropertyStorage(PropertyStorage value, StorageConfig config)
{
string propertyName = value.PropertyName;
string attributeName = value.AttributeName;
config.AddPropertyStorage(propertyName, value);
if (!AttributesToGet.Contains(attributeName))
AttributesToGet.Add(attributeName);
if (!ProjectionExpression.ExpressionAttributeNames.ContainsValue(attributeName))
AddAttributeNameToProjectionExpression(attributeName);
if (value.StoreAsEpoch)
AttributesToStoreAsEpoch.Add(attributeName);
if (value.StoreAsEpochLong)
AttributesToStoreAsEpochLong.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);
}
}
}
private void AddKeyPropertyNames(PropertyStorage value, string propertyName)
{
if (value.IsHashKey)
HashKeyPropertyNames.Add(propertyName);
if (value.IsRangeKey)
RangeKeyPropertyNames.Add(propertyName);
if (!value.IsVersion) return;
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 AddSearchVectorConfigs(List searchVectorIndexNames, string propertyName)
{
if (searchVectorIndexNames == null)
return;
foreach (var index in searchVectorIndexNames)
{
if (string.IsNullOrEmpty(index))
continue;
if (this.IndexNameToSearchVectorPropertiesMapping.TryGetValue(index, out var existingPropertyName) &&
!string.Equals(existingPropertyName, propertyName, StringComparison.Ordinal))
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
"Search vector index {0} is declared on multiple properties: {1} and {2}.", index, existingPropertyName, propertyName));
}
this.IndexNameToSearchVectorPropertiesMapping[index] = propertyName;
}
}
private void AddGSIConfigs(List gsiIndexNames, string propertyName, bool isHashKey, int order)
{
foreach (var index in gsiIndexNames)
{
GSIConfig gsiConfig = this.GetGSIConfig(index);
if (gsiConfig == null)
{
gsiConfig = new GSIConfig(index);
this.IndexNameToGSIMapping[index] = gsiConfig;
}
var targetDict = isHashKey ? gsiConfig.HashKeyPropertyNames : gsiConfig.RangeKeyPropertyNames;
string keyToRemove = null;
foreach (var kv in targetDict)
{
if (kv.Value != order) continue;
keyToRemove = kv.Key;
break;
}
if (keyToRemove != null)
{
targetDict.Remove(keyToRemove);
}
targetDict[propertyName] = order;
}
}
// constructor
internal ItemStorageConfig([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type targetType)
{
BaseTypeStorageConfig = new StorageConfig(targetType);
PolymorphicTypesStorageConfig = new Dictionary();
PolymorphicConfig= new Dictionary();
AttributeToIndexesNameMapping = new Dictionary>(StringComparer.Ordinal);
IndexNameToLSIRangePropertiesMapping = new Dictionary>(StringComparer.Ordinal);
IndexNameToGSIMapping = new Dictionary(StringComparer.Ordinal);
IndexNameToSearchVectorPropertiesMapping = new Dictionary(StringComparer.Ordinal);
AttributesToGet = new List();
ProjectionExpression = new Expression();
HashKeyPropertyNames = new List();
RangeKeyPropertyNames = new List();
AttributesToStoreAsEpoch = new HashSet();
AttributesToStoreAsEpochLong = 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; }
///
/// Variants of for a type that does not declare its own casing,
/// built with a casing inherited from an enclosing type. Keyed by the inherited
/// so a nested type used under differently-cased parents gets distinct,
/// correctly-baked configs (attribute names are baked at build time and cannot be re-cased
/// per parent otherwise).
///
public Dictionary InheritedCasingConfigs { get; private set; }
public ConfigTableCache(ItemStorageConfig baseTypeConfig)
{
BaseTypeConfig = baseTypeConfig;
BaseTableName = BaseTypeConfig.TableName;
Cache = new Dictionary(StringComparer.Ordinal);
InheritedCasingConfigs = new Dictionary();
}
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<[DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] T>(DynamoDBFlatConfig flatConfig, bool conversionOnly = false)
{
Type type = typeof(T);
return GetConfig(type, flatConfig, conversionOnly);
}
public ItemStorageConfig GetConfig([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] 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.
// Only propagate the nested type's conversion when it declares its own schema (via DynamoDBTable);
// a table-less nested type has a null Conversion and must not override the enclosing item's conversion.
if (conversionOnly)
{
if (flatConfig != null && tableCache.BaseTypeConfig.Conversion != null)
flatConfig.ItemConversion = tableCache.BaseTypeConfig.Conversion;
// Fast path: return an already-resolved config under the read lock. If an inherited
// casing variant is required but not yet built, TryResolveInheritedCasingConfig
// returns false; drop to the write lock to build it. We must NOT fall through to the
// table-cache lookup below, because if this nested type was previously converted as a
// root its table config is already cached and would be returned with the wrong
// (base) casing instead of the inherited variant.
if (TryResolveInheritedCasingConfig(type, tableCache, flatConfig, out var cachedConfig))
return cachedConfig;
}
else
{
actualTableName = DynamoDBContext.GetTableName(tableCache.BaseTableName, flatConfig);
if (tableCache.Cache.TryGetValue(actualTableName, out config))
{
if (flatConfig == null)
throw new ArgumentNullException("flatConfig");
flatConfig.ItemConversion = config.Conversion;
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, flatConfig);
flatConfig.ItemConversion = baseStorageConfig.Conversion;
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.
// Only propagate the nested type's conversion when it declares its own schema (via DynamoDBTable);
// a table-less nested type has a null Conversion and must not override the enclosing item's conversion.
if (conversionOnly)
{
if (flatConfig != null && tableCache.BaseTypeConfig.Conversion != null)
flatConfig.ItemConversion = tableCache.BaseTypeConfig.Conversion;
return ResolveInheritedCasingConfig(type, tableCache, flatConfig);
}
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, flatConfig);
flatConfig.ItemConversion = config.Conversion;
tableCache.Cache[actualTableName] = config;
return config;
}
finally
{
if(_readerWriterLockSlim.IsWriteLockHeld)
{
_readerWriterLockSlim.ExitWriteLock();
}
}
}
///
/// Determines whether an inherited (enclosing-type) casing should replace a nested type's own
/// casing. Inheritance applies only when the enclosing type resolved to
/// and the nested type did NOT explicitly declare its own casing
/// ( is false). A nested type that explicitly
/// declares any casing — including — is left untouched.
/// deliberately does not propagate, preserving PascalCase
/// nested objects for existing data.
///
private static bool ShouldInheritCasing(ConfigTableCache tableCache, DynamoDBFlatConfig flatConfig, out CaseMode inheritedMode)
{
inheritedMode = CaseMode.PascalCase;
if (flatConfig == null || !flatConfig.InheritedAttributeCasing.HasValue)
return false;
var candidate = flatConfig.InheritedAttributeCasing.Value;
// Defensive: only a casing that is itself inheritable may be applied to a nested type. In
// practice flatConfig.InheritedAttributeCasing is only ever set to an inheritable value (see
// Utils.GetInheritableCasing at the propagation sites), but keying off the helper here means
// this gate never hard-codes specific modes either.
if (Utils.GetInheritableCasing(candidate) != candidate)
return false;
// Skip pass-through casings (Unset/PascalCase): inheriting them would build a config variant
// whose baked names are identical to the base config, which is wasted work and cache space.
if (!Utils.IsNameTransformingCasing(candidate))
return false;
// Only fill in a nested type that did not explicitly declare a casing of its own. An explicit
// PascalCase declaration blocks inheritance (this is why CaseMode has an Unset sentinel:
// Unset means "not specified" and allows inheritance, whereas an explicit PascalCase does not).
if (tableCache.BaseTypeConfig.DeclaresOwnCasing)
return false;
inheritedMode = candidate;
return true;
}
///
/// Read-lock-safe resolution: returns the base config when no inheritance applies, or an already
/// built inherited-casing variant. Returns false (so the caller falls through to the write
/// lock) when a variant is required but not yet cached.
///
private bool TryResolveInheritedCasingConfig([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type type, ConfigTableCache tableCache, DynamoDBFlatConfig flatConfig, out ItemStorageConfig config)
{
if (!ShouldInheritCasing(tableCache, flatConfig, out var inheritedMode))
{
config = tableCache.BaseTypeConfig;
return true;
}
return tableCache.InheritedCasingConfigs.TryGetValue(inheritedMode, out config);
}
///
/// Write-lock resolution: returns the base config when no inheritance applies, or the cached
/// inherited-casing variant, building and caching it on first use. Must be called while holding
/// the write lock.
///
private ItemStorageConfig ResolveInheritedCasingConfig([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type type, ConfigTableCache tableCache, DynamoDBFlatConfig flatConfig)
{
if (!ShouldInheritCasing(tableCache, flatConfig, out var inheritedMode))
return tableCache.BaseTypeConfig;
if (!tableCache.InheritedCasingConfigs.TryGetValue(inheritedMode, out var variant))
{
variant = CreateStorageConfig(type, actualTableName: null, flatConfig, forcedCasing: inheritedMode);
tableCache.InheritedCasingConfigs[inheritedMode] = variant;
}
return variant;
}
private static string GetAccurateCase(ItemStorageConfig config, string value)
{
// Casing is baked into each attribute name at config-build time. The per-mode transform lives
// in Utils.ApplyCasing, so adding a new casing does not require changes here.
return Utils.ApplyCasing(config.AttributeCasing, value);
}
///
/// Resolves the effective for a type from its ,
/// reconciling the new property with the obsolete
/// flag.
///
/// Precedence:
/// 1. If is anything other than
/// (including an explicit ), it
/// wins and the type is considered to declare its own casing.
/// 2. Otherwise, if the obsolete
/// flag is true , the type maps to (camelCase root,
/// PascalCase nested) to exactly preserve the historical behavior of existing data, and is
/// considered to declare its own casing.
/// 2b. Otherwise, if was explicitly
/// assigned false (via the named property or the obsolete bool constructor), the type maps
/// to and IS considered to declare its own casing — preserving
/// the "PascalCase, do not camelCase" semantics of previous SDK versions so it does not inherit a CamelCase parent.
/// 3. Otherwise ( and no legacy flag) the type uses
/// and does NOT declare its own casing, so a nested instance
/// may inherit an enclosing type's casing.
///
/// Because is a distinct sentinel, an explicit
/// is distinguishable from "not specified"
/// ( ): the former declares casing (and blocks inheritance), the latter
/// does not.
///
private static CaseMode ResolveCaseMode(DynamoDBTableAttribute tableAttribute, Type type, out bool declaresOwnCasing)
{
#pragma warning disable CS0618 // Reconciling the obsolete LowerCamelCaseProperties flag and LegacyCamelCase mode.
if (tableAttribute.AttributeCasing != CaseMode.Unset)
{
// Reject an out-of-range value (e.g. (CaseMode)999) rather than letting it fall through to
// Utils.ApplyCasing, where an unrecognized mode would silently behave as PascalCase. An
// invalid casing is a configuration error, so fail fast at config-build time with a clear
// message rather than producing confusing attribute names at runtime.
if (!Enum.IsDefined(typeof(CaseMode), tableAttribute.AttributeCasing))
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
"Invalid AttributeCasing value '{0}' on type '{1}'. It must be a defined {2} value.",
(int)tableAttribute.AttributeCasing, type.FullName, nameof(CaseMode)));
}
declaresOwnCasing = true;
return tableAttribute.AttributeCasing;
}
if (tableAttribute.LowerCamelCaseProperties)
{
declaresOwnCasing = true;
return CaseMode.LegacyCamelCase;
}
// An explicit LowerCamelCaseProperties=false (via the named property or the obsolete bool
// constructor) is a deliberate "PascalCase, do not camelCase" choice from previous SDK
// versions, distinct from an
// omitted flag. Treat it as declaring its own casing so it blocks inheritance of an enclosing
// CamelCase parent (which would otherwise silently rename existing PascalCase attributes).
if (tableAttribute.LowerCamelCasePropertiesExplicitlySet)
{
declaresOwnCasing = true;
return CaseMode.PascalCase;
}
declaresOwnCasing = false;
return CaseMode.PascalCase;
#pragma warning restore CS0618
}
private ItemStorageConfig CreateStorageConfig([DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type baseType, string actualTableName, DynamoDBFlatConfig flatConfig, CaseMode? forcedCasing = null)
{
if (baseType == null)
throw new ArgumentNullException("baseType");
ItemStorageConfig config = new ItemStorageConfig(baseType);
PopulateConfigFromType(config, baseType, forcedCasing);
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, flatConfig.DisableFetchingTableMetadata);
}
catch
{
table = null;
}
if (table != null)
{
PopulateConfigFromTable(config, table);
}
}
config.Denormalize(Context, flatConfig.DerivedTypeAttributeName);
#if NET8_0_OR_GREATER
// Resolve binding-constructor arguments after Denormalize, which is where PropertyStorage.Validate
// assigns the effective converter. Running earlier would miss a default converter registered on the
// context (DynamoDBContext.ConverterCache is only consulted there). Mapping-level flags such as
// version, counter and ignore are already final by this point as well.
config.BaseTypeStorageConfig.ResolveConstructorArguments();
foreach (var polymorphicStorageConfig in config.PolymorphicTypesStorageConfig.Values)
{
polymorphicStorageConfig.ResolveConstructorArguments();
}
#endif
if (flatConfig.DisableFetchingTableMetadata)
{
if (string.IsNullOrEmpty(actualTableName))
{
actualTableName = DynamoDBContext.GetTableName(config.TableName, flatConfig);
}
var emptyConfig = new TableConfig(actualTableName, conversion: null, consumer: Table.DynamoDBConsumer.DataModel,
storeAsEpoch: null, storeAsEpochLong: null, isEmptyStringValueEnabled: false, metadataCachingMode: flatConfig.MetadataCachingMode);
var table = Table.CreateTableFromItemStorageConfig(Context.Client, emptyConfig, config, flatConfig);
// The table info must be cached under the actual table name exactly how it exists in the DynamoDB service.
// This is done to mimic the caching behavior when DisableFetchingTableMetadata is set to false.
Context.StoreUnconfiguredTable(actualTableName, table);
}
return config;
}
private static void PopulateConfigFromType(ItemStorageConfig config, [DynamicallyAccessedMembers(InternalConstants.DataModelModeledType)] Type type, CaseMode? forcedCasing = null)
{
DynamoDBTableAttribute tableAttribute = Utils.GetTableAttribute(type);
// A type declares its own casing when it has a [DynamoDBTable] with a non-null AttributeCasing
// (including an explicit PascalCase) or the obsolete LowerCamelCaseProperties flag. Such a type
// is never overridden by an inherited (forced) casing, so an explicit PascalCase blocks
// inheritance of an enclosing CamelCase.
bool declaresOwnCasing = false;
if (tableAttribute == null)
{
config.TableName = type.Name;
}
else
{
if (string.IsNullOrEmpty(tableAttribute.TableName)) throw new InvalidOperationException("DynamoDBTableAttribute.Table is empty or null");
config.TableName = tableAttribute.TableName;
config.AttributeCasing = ResolveCaseMode(tableAttribute, type, out declaresOwnCasing);
config.Conversion = tableAttribute.Conversion switch
{
ConversionSchema.V1 => DynamoDBEntryConversion.V1,
ConversionSchema.V2 => DynamoDBEntryConversion.V2,
_ => config.Conversion
};
}
// Inherit the enclosing type's casing for a nested type that does not declare its own.
if (forcedCasing.HasValue && !declaresOwnCasing)
config.AttributeCasing = forcedCasing.Value;
config.DeclaresOwnCasing = declaresOwnCasing;
string tableAlias;
if (AWSConfigsDynamoDB.Context.TableAliases.TryGetValue(config.TableName, out tableAlias))
config.TableName = tableAlias;
ICollection baseConstructorParameterNames = null;
#if NET8_0_OR_GREATER
baseConstructorParameterNames = config.BaseTypeStorageConfig.ConstructorParameterNames;
#endif
var members = Utils.GetMembersFromType(type, baseConstructorParameterNames);
foreach (var member in members)
{
var propertyStorage = MemberInfoToPropertyStorage(config, member);
if (!propertyStorage.IsIgnored)
{
ValidateAttributeName(config, propertyStorage.AttributeName);
}
config.BaseTypeStorageConfig.Properties.Add(propertyStorage);
}
DynamoDBPolymorphicTypeAttribute[] polymorphicTypeAttribute = Utils.GetPolymorphicTypesAttribute(type);
if (polymorphicTypeAttribute is not { Length: > 0 }) return;
{
foreach (var attribute in polymorphicTypeAttribute)
{
if (attribute.DerivedType == null)
{
throw new InvalidOperationException("Invalid polymorphic type: DerivedType is null.");
}
if (!attribute.DerivedType.IsSubclassOf(type))
{
throw new InvalidOperationException($"Invalid polymorphic type: '{attribute.DerivedType.FullName}' must be a subclass of '{type.FullName}'.");
}
var polymorphicStorageConfig = new StorageConfig(attribute.DerivedType);
ICollection polymorphicConstructorParameterNames = null;
#if NET8_0_OR_GREATER
polymorphicConstructorParameterNames = polymorphicStorageConfig.ConstructorParameterNames;
#endif
var polymorphicTypeMembers = Utils.GetMembersFromType(attribute.DerivedType, polymorphicConstructorParameterNames);
foreach (var member in polymorphicTypeMembers)
{
var propertyStorage = MemberInfoToPropertyStorage(config, member);
polymorphicStorageConfig.Properties.Add(propertyStorage);
}
config.AddPolymorphicPropertyStorageConfiguration(attribute.TypeDiscriminator, attribute.DerivedType, polymorphicStorageConfig);
}
}
}
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072",
Justification = "The user's type has been annotated with DynamicallyAccessedMemberTypes.All with the public API into the library. At this point the type will not be trimmed.")]
private static PropertyStorage MemberInfoToPropertyStorage(ItemStorageConfig config, MemberInfo member)
{
// prepare basic info
PropertyStorage propertyStorage = new PropertyStorage(member);
propertyStorage.AttributeName = GetAccurateCase(config, member.Name);
// run through all DDB attributes
List allAttributes = Utils.GetAttributes(member);
if(allAttributes.Count>1 &&
allAttributes.Any(a => a is DynamoDBFlattenAttribute))
{
throw new InvalidOperationException("DynamoDBFlatten cannot be combined with other annotations.");
}
foreach (var attribute in allAttributes)
{
// filter out ignored properties
if (attribute is DynamoDBIgnoreAttribute)
{
propertyStorage.IsIgnored = true;
continue;
}
// flatten properties
if (attribute is DynamoDBFlattenAttribute)
{
propertyStorage.ShouldFlattenChildProperties = true;
var type = Utils.GetType(member);
if (Utils.IsCollectionType(type) || Utils.IsPrimitive(type))
{
throw new InvalidOperationException("Cannot flatten primitive types or collections. Only complex objects are supported.");
}
#if NET8_0_OR_GREATER
// A flattened value is materialized without invoking its constructor: a reference type through
// InstantiateConverter, which requires a parameterless constructor, and a value type by
// zero-initialization followed by member assignment (see CreateFlattenedMember). A value type is
// therefore usable as long as every member it persists can be assigned afterwards, which covers a
// mutable struct with a parameterized constructor and a positional record struct, whose init
// accessors are writable. A get-only constructor-bound member would be written when the item is
// saved and never read back, so that shape is still rejected.
if (Utils.TryGetBindingConstructor(type, out var flattenedBindingConstructor))
{
bool constructorOnlyMembers = Utils.HasConstructorOnlyMembers(type, flattenedBindingConstructor);
if (!type.IsValueType || constructorOnlyMembers)
{
string reason = !type.IsValueType
? "it is a reference type populated through a constructor, and a flattened value is reconstructed without invoking that constructor"
: "it has members that only its constructor can populate (get-only properties matching a constructor parameter), which would be written when the item is saved and never read back";
throw new InvalidOperationException(
$"Property '{propertyStorage.PropertyName}' is marked [DynamoDBFlatten] but its type {type.FullName} cannot be flattened because {reason}. " +
"Use a type with a parameterless constructor and settable members, a struct or record struct whose members are all settable (init counts as settable), " +
"or store it as a nested (non-flattened) property.");
}
}
#endif
// A flattened value is materialized by CreateFlattenedMember when the item is loaded. Reject a
// type it cannot materialize here, at configuration time, so that saving and loading fail the
// same way with a message that names the property. Without this the failure surfaces only while
// loading, as a bare "Cannot instantiate type" that does not say which member caused it.
bool canMaterializeFlattenedType = Utils.CanInstantiateConverter(type);
#if NET8_0_OR_GREATER
// Value types are materialized by zero-initialization instead (see CreateFlattenedMember).
canMaterializeFlattenedType = canMaterializeFlattenedType || type.IsValueType;
#endif
if (!canMaterializeFlattenedType)
{
throw new InvalidOperationException(
$"Property '{propertyStorage.PropertyName}' is marked [DynamoDBFlatten] but its type {type.FullName} cannot be instantiated. " +
"A flattened value is reconstructed when the item is loaded, which requires a public parameterless constructor " +
"(or one accepting a DynamoDBContext) and settable members. Give the type such a constructor, or store it as a " +
"nested (non-flattened) property.");
}
var members = Utils.GetMembersFromType(type);
// Flattened children are serialized on save through their own resolved config
// (SerializeToDocument), so their attribute names use the CHILD's effective casing. That
// is: the child's own declared casing if it has one; otherwise the casing the parent
// actually PROPAGATES to an undecorated child, i.e. Utils.GetInheritableCasing(parent).
// The latter matters for non-propagating parent modes: a LegacyCamelCase (or PascalCase/
// Unset) parent does not cascade, so an undecorated child serializes as PascalCase on
// save and the FlattenProperties metadata must be baked as PascalCase to match on load.
var childTableAttribute = Utils.GetTableAttribute(type);
CaseMode effectiveChildCasing;
if (childTableAttribute != null &&
ResolveCaseMode(childTableAttribute, type, out var childDeclaresOwnCasing) is var childCasing &&
childDeclaresOwnCasing)
{
// Child declares its own casing; save uses it directly (inheritance is blocked).
effectiveChildCasing = childCasing;
}
else
{
// Undecorated child: adopt exactly what the parent propagates on save. Non-propagating
// parent modes (LegacyCamelCase, PascalCase, Unset) leave the child at PascalCase.
effectiveChildCasing = Utils.GetInheritableCasing(config.AttributeCasing) ?? CaseMode.PascalCase;
}
// Bake names with the child's effective casing. Reuse the parent config when the casing
// already matches; otherwise use a lightweight config carrying the child's casing.
propertyStorage.FlattenedEffectiveCasing = effectiveChildCasing;
var flattenNameConfig = effectiveChildCasing == config.AttributeCasing
? config
: new ItemStorageConfig(type) { AttributeCasing = effectiveChildCasing };
foreach (var memberInfo in members)
{
var flattenPropertyStorage = MemberInfoToPropertyStorage(flattenNameConfig, memberInfo);
flattenPropertyStorage.IsFlattened = true;
// Stamp the flattened child's effective casing onto each leaf. A condition
// (ScanCondition/QueryCondition) or expression can target a flattened complex leaf by
// its property name; when its value is a complex object, the value's Map keys must be
// cased with the flattened child's effective casing (which may differ from the root's,
// e.g. an explicitly-PascalCase flattened child under a CamelCase root), not the
// root's casing.
//
// A nested [DynamoDBFlatten] node is itself the result of a recursive
// MemberInfoToPropertyStorage call above, which already computed and stored ITS own
// effective casing. Do not overwrite that with the enclosing node's casing — otherwise
// a complex leaf inside the nested flatten node is (de)serialized with the wrong
// casing. Only stamp true leaves (those that are not themselves flatten nodes).
if (!flattenPropertyStorage.ShouldFlattenChildProperties)
flattenPropertyStorage.FlattenedEffectiveCasing = effectiveChildCasing;
propertyStorage.FlattenProperties.Add(flattenPropertyStorage);
}
}
if (attribute is DynamoDBVersionAttribute)
propertyStorage.IsVersion = true;
DynamoDBAtomicCounterAttribute counterAttribute = attribute as DynamoDBAtomicCounterAttribute;
if (counterAttribute != null)
{
propertyStorage.IsCounter = true;
propertyStorage.CounterDelta = counterAttribute.Delta;
propertyStorage.CounterStartValue = counterAttribute.StartValue;
}
DynamoDBSearchVectorAttribute searchVectorAttribute = attribute as DynamoDBSearchVectorAttribute;
if (searchVectorAttribute != null)
{
propertyStorage.IsSearchVector = true;
if (!string.IsNullOrEmpty(searchVectorAttribute.IndexName))
propertyStorage.SearchVectorIndexNames.Add(searchVectorAttribute.IndexName);
}
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)
{
#pragma warning disable CS0618 // Type or member is obsolete
propertyStorage.StoreAsEpoch = propertyAttribute.StoreAsEpoch;
#pragma warning restore CS0618 // Type or member is obsolete
propertyStorage.StoreAsEpochLong = propertyAttribute.StoreAsEpochLong;
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;
}
if (propertyAttribute is DynamoDBAutoGeneratedTimestampAttribute)
{
propertyStorage.IsAutoGeneratedTimestamp = true;
config.HasAutogeneratedProperties = true;
}
if (propertyAttribute is DynamoDbUpdateBehaviorAttribute updateBehaviorAttribute)
{
propertyStorage.UpdateBehaviorMode = updateBehaviorAttribute.Behavior;
}
DynamoDBLocalSecondaryIndexRangeKeyAttribute lsiRangeKeyAttribute = propertyAttribute as DynamoDBLocalSecondaryIndexRangeKeyAttribute;
if (lsiRangeKeyAttribute != null)
{
propertyStorage.IsLSIRangeKey = true;
propertyStorage.AddIndex(lsiRangeKeyAttribute);
}
}
DynamoDBPolymorphicTypeAttribute polymorphicAttribute = attribute as DynamoDBPolymorphicTypeAttribute;
if (polymorphicAttribute != null)
{
propertyStorage.PolymorphicProperty = true;
propertyStorage.AddDerivedType(polymorphicAttribute.TypeDiscriminator, polymorphicAttribute.DerivedType);
}
}
return 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;
var hashKeyOrder = 0;
var rangeKeyOrder = 0;
var keyOrder = 0;
foreach (var element in gsi.KeySchema)
{
string attributeName = element.AttributeName;
bool isHashKey = element.KeyType == KeyType.HASH;
property = GetProperty(config, attributeName, true);
if (property != null)
{
keyOrder = isHashKey ? hashKeyOrder : rangeKeyOrder;
// Check for existing properties with same GSI configuration and remove index
foreach (var existingProperty in config.BaseTypeStorageConfig.Properties)
{
var gsiToModify = existingProperty.Indexes.OfType()
.FirstOrDefault(gsiIndex => gsiIndex.IndexNames.Contains(indexName) &&
gsiIndex.Order == keyOrder &&
gsiIndex.IsHashKey == isHashKey);
if (gsiToModify != null)
{
gsiToModify.IndexNames.Remove(indexName);
if (gsiToModify.IndexNames.Count == 0)
{
existingProperty.Indexes.Remove(gsiToModify);
}
}
}
property.AddGsiIndex(isHashKey, attributeName, keyOrder, indexName);
if (isHashKey)
{
hashKeyOrder++;
}
else
{
rangeKeyOrder++;
}
}
}
}
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.BaseTypeStorageConfig.TargetType;
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.BaseTypeStorageConfig.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;
propertyStorage.StoreAsEpochLong = propertyConfig.StoreAsEpochLong;
}
}
}
// 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.BaseTypeStorageConfig.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.BaseTypeStorageConfig.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.BaseTypeStorageConfig.Properties.Add(property);
}
}
return property;
}
private static List GetIndexProperties(ItemStorageConfig config, string indexName)
{
return config.BaseTypeStorageConfig.Properties
.Where(p => p.Indexes != null && p.Indexes.Any(gsi => gsi.IndexNames.Contains(indexName)))
.ToList();
}
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());
}
}
private static void ValidateAttributeName(ItemStorageConfig config, string attributeName)
{
foreach (var property in config.BaseTypeStorageConfig.Properties)
{
if (string.Equals(property.AttributeName, attributeName) && !property.IsIgnored)
{
throw new InvalidOperationException(
$"Attempt to add an attribute that is already defined.[Attribute name: {attributeName}]");
}
}
}
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);
}
}
}