Skip to content
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."
]
}
]
}
103 changes: 103 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,107 @@ 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)
{
}
}

/// <summary>
/// Specifies the update behavior for a property when performing DynamoDB update operations.
/// This attribute can be used to control whether a property is always updated, only updated if not null.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public sealed class DynamoDbUpdateBehaviorAttribute : DynamoDBPropertyAttribute
{
/// <summary>
/// Gets the update behavior for the property.
/// </summary>
public UpdateBehavior Behavior { get; }

/// <summary>
/// Default constructor. Sets behavior to Always.
/// </summary>
public DynamoDbUpdateBehaviorAttribute()
: base()
{
Behavior = UpdateBehavior.Always;
}

/// <summary>
/// Constructor that specifies the update behavior.
/// </summary>
/// <param name="behavior">The update behavior to apply.</param>
public DynamoDbUpdateBehaviorAttribute(UpdateBehavior behavior)
: base()
{
Behavior = behavior;
}

/// <summary>
/// Constructor that specifies an alternate attribute name and update behavior.
/// </summary>
/// <param name="attributeName">Name of attribute to be associated with property or field.</param>
/// <param name="behavior">The update behavior to apply.</param>
public DynamoDbUpdateBehaviorAttribute(string attributeName, UpdateBehavior behavior)
: base(attributeName)
{
Behavior = behavior;
}
}

/// <summary>
/// Specifies when a property value should be set.
/// </summary>
public enum UpdateBehavior
{
/// <summary>
/// Set the value on both create and update.
/// </summary>
Always,
/// <summary>
/// Set the value only when the item is created.
/// </summary>
IfNotExists
}
}
90 changes: 49 additions & 41 deletions sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -374,31 +374,37 @@ public IMultiTableTransactWrite CreateMultiTableTransactWrite(params ITransactWr

Document updateDocument;
Expression versionExpression = null;

var returnValues=counterConditionExpression == null ? ReturnValues.None : ReturnValues.AllNewAttributes;

if ((flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) || !storage.Config.HasVersion)
SetNewTimestamps(storage);

var updateIfNotExists = GetUpdateIfNotExistsAttributeNames(storage);

var returnValues = counterConditionExpression == null && !updateIfNotExists.Any()
? ReturnValues.None
: ReturnValues.AllNewAttributes;

var updateItemOperationConfig = new UpdateItemOperationConfig
{
updateDocument = table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), new UpdateItemOperationConfig()
{
ReturnValues = returnValues
}, counterConditionExpression);
}
else
ReturnValues = returnValues
};

if (!(flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) && storage.Config.HasVersion)
{
var conversionConfig = new DynamoDBEntry.AttributeConversionConfig(table.Conversion, table.IsEmptyStringValueEnabled);
var conversionConfig = new DynamoDBEntry.AttributeConversionConfig(table.Conversion, table.IsEmptyStringValueEnabled);
versionExpression = CreateConditionExpressionForVersion(storage, conversionConfig);
SetNewVersion(storage);

var updateItemOperationConfig = new UpdateItemOperationConfig
{
ReturnValues = returnValues,
ConditionalExpression = versionExpression,
};
updateDocument = table.UpdateHelper(storage.Document, table.MakeKey(storage.Document), updateItemOperationConfig, counterConditionExpression);
updateItemOperationConfig.ConditionalExpression = versionExpression;
}

if (counterConditionExpression == null && versionExpression == null) return;
updateDocument = table.UpdateHelper(
storage.Document,
table.MakeKey(storage.Document),
updateItemOperationConfig,
counterConditionExpression,
updateIfNotExists
);

if (counterConditionExpression == null && versionExpression == null && !updateIfNotExists.Any()) return;

if (returnValues == ReturnValues.AllNewAttributes)
{
Expand Down Expand Up @@ -427,36 +433,38 @@ private async Task SaveHelperAsync([DynamicallyAccessedMembers(InternalConstants
Document updateDocument;
Expression versionExpression = null;

var returnValues = counterConditionExpression == null ? ReturnValues.None : ReturnValues.AllNewAttributes;
SetNewTimestamps(storage);

var updateIfNotExistsAttributeName = GetUpdateIfNotExistsAttributeNames(storage);

if (
(flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value)
|| !storage.Config.HasVersion)
var returnValues = counterConditionExpression == null && !updateIfNotExistsAttributeName.Any()
? ReturnValues.None
: ReturnValues.AllNewAttributes;

var updateItemOperationConfig = new UpdateItemOperationConfig
{
updateDocument = await table.UpdateHelperAsync(storage.Document, table.MakeKey(storage.Document), new UpdateItemOperationConfig
{
ReturnValues = returnValues
}, counterConditionExpression, cancellationToken).ConfigureAwait(false);
}
else
ReturnValues = returnValues
};

if (!(flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value) && storage.Config.HasVersion)
{
var conversionConfig = new DynamoDBEntry.AttributeConversionConfig(table.Conversion, table.IsEmptyStringValueEnabled);
var conversionConfig = new DynamoDBEntry.AttributeConversionConfig(table.Conversion, table.IsEmptyStringValueEnabled);
versionExpression = CreateConditionExpressionForVersion(storage, conversionConfig);
SetNewVersion(storage);

updateDocument = await table.UpdateHelperAsync(
storage.Document,
table.MakeKey(storage.Document),
new UpdateItemOperationConfig
{
ReturnValues = returnValues,
ConditionalExpression = versionExpression
}, counterConditionExpression,
cancellationToken)
.ConfigureAwait(false);
updateItemOperationConfig.ConditionalExpression = versionExpression;
}

if (counterConditionExpression == null && versionExpression == null) return;
updateDocument = await table.UpdateHelperAsync(
storage.Document,
table.MakeKey(storage.Document),
updateItemOperationConfig,
counterConditionExpression,
cancellationToken,
updateIfNotExistsAttributeName
).ConfigureAwait(false);


if (counterConditionExpression == null && versionExpression == null && !updateIfNotExistsAttributeName.Any()) return;

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

namespace Amazon.DynamoDBv2.DataModel
Expand Down Expand Up @@ -59,6 +60,7 @@ internal static void SetNewVersion(ItemStorage storage)
}
storage.Document[versionAttributeName] = version;
}

private static void IncrementVersion(Type memberType, ref Primitive version)
{
if (memberType.IsAssignableFrom(typeof(Byte))) version = version.AsByte() + 1;
Expand Down Expand Up @@ -118,6 +120,40 @@ internal static Expression CreateConditionExpressionForVersion(ItemStorage stora

#endregion

#region Autogenerated Timestamp

internal static void SetNewTimestamps(ItemStorage storage)
{
var timestampProperties = GetTimestampProperties(storage);
if (timestampProperties.Length == 0) return;

var now = AWSSDKUtils.CorrectedUtcNow;

foreach (var timestampProperty in timestampProperties)
{
var attributeName = timestampProperty.AttributeName;

storage.Document[attributeName] = new Primitive(now.ToString("o"));
}
}

internal static bool HasCreateOnlyProperties(ItemStorage storage)
{
return storage.Config.BaseTypeStorageConfig.AllPropertyStorage
.Any(propertyStorage => propertyStorage.UpdateBehaviorMode == UpdateBehavior.IfNotExists);
}

private static PropertyStorage[] GetTimestampProperties(ItemStorage storage)
{
//todo : adapt this to work with polymorphic types
var counterProperties = storage.Config.BaseTypeStorageConfig.AllPropertyStorage.
Where(propertyStorage => propertyStorage.IsAutoGeneratedTimestamp).ToArray();

return counterProperties;
}

#endregion

#region Atomic counters

internal static Expression BuildCounterConditionExpression(ItemStorage storage)
Expand All @@ -135,7 +171,7 @@ internal static Expression BuildCounterConditionExpression(ItemStorage storage)

private static PropertyStorage[] GetCounterProperties(ItemStorage storage)
{
var counterProperties = storage.Config.BaseTypeStorageConfig.Properties.
var counterProperties = storage.Config.BaseTypeStorageConfig.AllPropertyStorage.
Where(propertyStorage => propertyStorage.IsCounter).ToArray();

return counterProperties;
Expand Down Expand Up @@ -163,10 +199,16 @@ private static Expression CreateUpdateExpressionForCounterProperties(PropertySto
propertyStorage.CounterStartValue - propertyStorage.CounterDelta;
}
updateExpression.ExpressionStatement = $"SET {asserts.Substring(0, asserts.Length - 2)}";

return updateExpression;
}

internal static List<string> GetUpdateIfNotExistsAttributeNames(ItemStorage storage)
{
var timestampProperties = storage.Config.BaseTypeStorageConfig.AllPropertyStorage
.Where(propertyStorage => propertyStorage.UpdateBehaviorMode == UpdateBehavior.IfNotExists).ToArray();
return timestampProperties.Select(p => p.AttributeName).ToList();
}

#endregion

#region Table methods
Expand Down Expand Up @@ -570,6 +612,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 Down
Loading