Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions generator/.DevConfigs/c952ab1e-3056-4598-9d0e-f7f02187e982.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"services": [
{
"serviceName": "DynamoDBv2",
"type": "patch",
"changeLogMessages": [
"Add support for DynamoDBAutoGeneratedTimestampAttribute that sets current timestamp during persistence operations."
]
}
]
}
45 changes: 45 additions & 0 deletions sdk/src/Services/DynamoDBv2/Custom/DataModel/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -688,4 +688,49 @@ public DynamoDBLocalSecondaryIndexRangeKeyAttribute(params string[] indexNames)
IndexNames = indexNames.Distinct(StringComparer.Ordinal).ToArray();
}
}

/// <summary>
/// Specifies that the decorated property or field should have its value automatically
/// set to the current timestamp during persistence operations.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public sealed class DynamoDBAutoGeneratedTimestampAttribute : DynamoDBPropertyAttribute
{

/// <summary>
/// Default constructor. Timestamp is set on both create and update.
/// </summary>
public DynamoDBAutoGeneratedTimestampAttribute()
: base()
{
}


/// <summary>
/// Constructor that specifies an alternate attribute name.
/// </summary>
/// <param name="attributeName">Name of attribute to be associated with property or field.</param>
public DynamoDBAutoGeneratedTimestampAttribute(string attributeName)
: base(attributeName)
{
}
/// <summary>
/// Constructor that specifies a custom converter.
/// </summary>
/// <param name="converter">Custom converter type.</param>
public DynamoDBAutoGeneratedTimestampAttribute([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type converter)
: base(converter)
{
}

/// <summary>
/// Constructor that specifies an alternate attribute name and a custom converter.
/// </summary>
/// <param name="attributeName">Name of attribute to be associated with property or field.</param>
/// <param name="converter">Custom converter type.</param>
public DynamoDBAutoGeneratedTimestampAttribute(string attributeName, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type converter)
: base(attributeName, converter)
{
}
}
}
2 changes: 1 addition & 1 deletion sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ private async Task SaveHelperAsync([DynamicallyAccessedMembers(InternalConstants
.ConfigureAwait(false);
}

if (counterConditionExpression == null && versionExpression == null) return;
if (counterConditionExpression == null && versionExpression == null && !storage.Config.HasAutogeneratedProperties) return;

if (returnValues == ReturnValues.AllNewAttributes)
{
Expand Down
29 changes: 27 additions & 2 deletions sdk/src/Services/DynamoDBv2/Custom/DataModel/ContextInternal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
using System.Globalization;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using Amazon.Util;
using ThirdParty.RuntimeBackports;
using Expression = System.Linq.Expressions.Expression;

Expand Down Expand Up @@ -139,7 +140,16 @@ private static PropertyStorage[] GetCounterProperties(ItemStorage storage)
{
var counterProperties = storage.Config.BaseTypeStorageConfig.Properties.
Where(propertyStorage => propertyStorage.IsCounter).ToArray();

var flatten= storage.Config.BaseTypeStorageConfig.Properties.
Where(propertyStorage => propertyStorage.FlattenProperties.Any()).ToArray();
while (flatten.Any())
{
var flattenCounters = flatten.SelectMany(p => p.FlattenProperties.Where(fp => fp.IsCounter)).ToArray();
counterProperties = counterProperties.Concat(flattenCounters).ToArray();
flatten = flatten.SelectMany(p => p.FlattenProperties.Where(fp => fp.FlattenProperties.Any())).ToArray();
}


return counterProperties;
}

Expand Down Expand Up @@ -541,6 +551,8 @@ private void PopulateItemStorage(object toStore, ItemStorage storage, DynamoDBFl
storageConfig = config.BaseTypeStorageConfig;
}

var now = AWSSDKUtils.CorrectedUtcNow;

foreach (PropertyStorage propertyStorage in storageConfig.AllPropertyStorage)
{
// if only keys are being serialized, skip non-key properties
Expand All @@ -557,7 +569,7 @@ private void PopulateItemStorage(object toStore, ItemStorage storage, DynamoDBFl
object value;
if (TryGetValue(toStore, propertyStorage.Member, out value))
{
DynamoDBEntry dbe = ToDynamoDBEntry(propertyStorage, value, flatConfig, propertyStorage.ShouldFlattenChildProperties);
DynamoDBEntry dbe = ToDynamoDBEntry(propertyStorage, value, flatConfig);

if (ShouldSave(dbe, ignoreNullValues))
{
Expand All @@ -572,6 +584,14 @@ private void PopulateItemStorage(object toStore, ItemStorage storage, DynamoDBFl
{
document[pair.Key] = pair.Value;
}

if (propertyStorage.FlattenProperties.Any(p => p.IsVersion))
{
var innerVersionProperty =
propertyStorage.FlattenProperties.First(p => p.IsVersion);
storage.CurrentVersion =
innerDocument[innerVersionProperty.AttributeName] as Primitive;
}
}
else
{
Expand All @@ -590,6 +610,11 @@ private void PopulateItemStorage(object toStore, ItemStorage storage, DynamoDBFl
storage.CurrentVersion = dbePrimitive;
}
}

if (dbe == null && propertyStorage.IsAutoGeneratedTimestamp)
{
document[attributeName] = new Primitive(now.ToString("o"));
}
}
else
throw new InvalidOperationException(
Expand Down
40 changes: 35 additions & 5 deletions sdk/src/Services/DynamoDBv2/Custom/DataModel/InternalModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,21 +137,36 @@ internal class PropertyStorage : SimplePropertyStorage
public bool IsGSIKey { get { return IsGSIHashKey || IsGSIRangeKey; } }
public bool IsIgnored { get; set; }

// whether to store DateTime as epoch seconds integer
/// <summary>
/// Whether to store DateTime as epoch seconds integer.
/// </summary>
public bool StoreAsEpoch { get; set; }

// whether to store DateTime as epoch seconds integer (with support for dates AFTER 2038)
/// <summary>
/// Whether to store DateTime as epoch seconds integer (with support for dates AFTER 2038).
/// </summary>
public bool StoreAsEpochLong { get; set; }

// whether to store Type Discriminator for polymorphic serialization
/// <summary>
/// Whether to store Type Discriminator for polymorphic serialization.
/// </summary>
public bool PolymorphicProperty { get; set; }

// whether to store child properties at the same level as the parent property
/// <summary>
/// Whether to store child properties at the same level as the parent property.
/// </summary>
public bool ShouldFlattenChildProperties { get; set; }

// whether to store property at parent level
/// <summary>
/// Whether to store property at parent level.
/// </summary>
public bool IsFlattened { get; set; }

/// <summary>
/// Whether to store the property as a timestamp that is automatically generated.
/// </summary>
public bool IsAutoGeneratedTimestamp { get; set; }

// corresponding IndexNames, if applicable
public List<string> IndexNames { get; set; }

Expand Down Expand Up @@ -242,6 +257,9 @@ public void Validate(DynamoDBContext context)
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");

Expand All @@ -250,6 +268,9 @@ public void Validate(DynamoDBContext context)

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);

IPropertyConverter converter;
if (context.ConverterCache.TryGetValue(MemberType, out converter) && converter != null)
Expand Down Expand Up @@ -470,6 +491,8 @@ internal class ItemStorageConfig
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<string, List<string>> AttributeToIndexesNameMapping { get; set; }

Expand Down Expand Up @@ -1082,6 +1105,13 @@ private static PropertyStorage MemberInfoToPropertyStorage(ItemStorageConfig con
propertyStorage.IsRangeKey = true;
}

if (propertyAttribute is DynamoDBAutoGeneratedTimestampAttribute)
{
propertyStorage.IsAutoGeneratedTimestamp = true;
config.HasAutogeneratedProperties = true;

}

DynamoDBLocalSecondaryIndexRangeKeyAttribute lsiRangeKeyAttribute = propertyAttribute as DynamoDBLocalSecondaryIndexRangeKeyAttribute;
if (lsiRangeKeyAttribute != null)
{
Expand Down
15 changes: 15 additions & 0 deletions sdk/src/Services/DynamoDBv2/Custom/DataModel/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,21 @@ internal static void ValidateNumericType(Type memberType)
throw new InvalidOperationException("Version property must be of primitive, numeric, integer, nullable type (e.g. int?, long?, byte?)");
}

internal static void ValidateTimestampType(Type memberType)
{
if (memberType.IsGenericType && memberType.GetGenericTypeDefinition() == typeof(Nullable<>) &&
(memberType.IsAssignableFrom(typeof(DateTime)) ||
memberType.IsAssignableFrom(typeof(DateTimeOffset))))
{
return;
}
throw new InvalidOperationException(
$"Timestamp properties must be of type Nullable<DateTime> (DateTime?) or Nullable<DateTimeOffset> (DateTimeOffset?). " +
$"Invalid type: {memberType.FullName}. " +
"Please ensure your property is declared as 'DateTime?' or 'DateTimeOffset?'."
);
}

[return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)]
internal static Type GetPrimitiveElementType(Type collectionType)
{
Expand Down
Loading