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
}
}
86 changes: 45 additions & 41 deletions sdk/src/Services/DynamoDBv2/Custom/DataModel/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -375,31 +375,35 @@ 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)
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,37 +431,37 @@ private async Task SaveHelperAsync([DynamicallyAccessedMembers(InternalConstants

Document updateDocument;
Expression versionExpression = null;

var updateIfNotExistsAttributeName = GetUpdateIfNotExistsAttributeNames(storage);

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

if (
(flatConfig.SkipVersionCheck.HasValue && flatConfig.SkipVersionCheck.Value)
|| !storage.Config.HasVersion)
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
31 changes: 27 additions & 4 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 @@ -61,6 +62,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 @@ -137,7 +139,7 @@ internal static DocumentModel.Expression BuildCounterConditionExpression(ItemSto

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 @@ -165,10 +167,16 @@ private static DocumentModel.Expression CreateUpdateExpressionForCounterProperti
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 @@ -540,6 +548,7 @@ private void PopulateItemStorage(object toStore, ItemStorage storage, DynamoDBFl
{
storageConfig = config.BaseTypeStorageConfig;
}
var now = AWSSDKUtils.CorrectedUtcNow;

foreach (PropertyStorage propertyStorage in storageConfig.AllPropertyStorage)
{
Expand All @@ -557,11 +566,10 @@ 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))
{

if (propertyStorage.ShouldFlattenChildProperties)
{
if (dbe == null) continue;
Expand All @@ -572,6 +580,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 @@ -588,8 +604,15 @@ private void PopulateItemStorage(object toStore, ItemStorage storage, DynamoDBFl

if (propertyStorage.IsVersion)
storage.CurrentVersion = dbePrimitive;

}
}

if (dbe == null && propertyStorage.IsAutoGeneratedTimestamp)
{
document[attributeName] = new Primitive(now.ToString("o"));
}

}
else
throw new InvalidOperationException(
Expand Down
Loading