Skip to content
This repository has been archived by the owner on Dec 24, 2022. It is now read-only.

Commit

Permalink
Add support for typed Update Expression builder with Conditional Expr…
Browse files Browse the repository at this point in the history
…ession
  • Loading branch information
mythz committed Jun 23, 2016
1 parent 39afe57 commit 8d48b13
Show file tree
Hide file tree
Showing 6 changed files with 326 additions and 13 deletions.
11 changes: 11 additions & 0 deletions src/ServiceStack.Aws/DynamoDb/IPocoDynamo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ public interface IPocoDynamo : IPocoDynamoAsync, IRequiresSchema
/// </summary>
T PutItem<T>(T value, bool returnOld = false);

/// <summary>
/// Creates an Typed `UpdateExpression` for the specified table
/// </summary>
UpdateExpression<T> UpdateExpression<T>(object hash, object range = null);

/// <summary>
/// Calls 'UpdateItem' to SET, REMOVE, ADD or DELETE item fields in DynamoDB.
/// </summary>
/// <returns>false if conditional check failed, otherwise true</returns>
bool UpdateItem<T>(UpdateExpression<T> update);

/// <summary>
/// Calls 'UpdateItem' to ADD, PUT or DELETE item fields in DynamoDB
/// </summary>
Expand Down
18 changes: 18 additions & 0 deletions src/ServiceStack.Aws/DynamoDb/PocoDynamo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,24 @@ public T PutItem<T>(T value, bool returnOld = false)
return Converters.FromAttributeValues<T>(table, response.Attributes);
}

public UpdateExpression<T> UpdateExpression<T>(object hash, object range=null)
{
return new UpdateExpression<T>(this, DynamoMetadata.GetTable<T>(), hash, range);
}

public bool UpdateItem<T>(UpdateExpression<T> update)
{
try
{
Exec(() => DynamoDb.UpdateItem(update));
return true;
}
catch (ConditionalCheckFailedException ex)
{
return false;
}
}

public void UpdateItem<T>(DynamoUpdateItem update)
{
var table = DynamoMetadata.GetTable<T>();
Expand Down
27 changes: 14 additions & 13 deletions src/ServiceStack.Aws/DynamoDb/PocoDynamoExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public static void DeleteItems<T>(this IPocoDynamo db, IEnumerable<string> ids)
});
}

static T[] ToArraySafe<T>(this IEnumerable<T> items)
internal static T[] ToArraySafe<T>(this IEnumerable<T> items)
{
return items == null
? null
Expand Down Expand Up @@ -196,7 +196,7 @@ public static List<T> QueryInto<T>(this IPocoDynamo db, QueryExpression request,
return db.Query<T>(request.Projection<T>(), limit: limit);
}

static AttributeValue NullValue = new AttributeValue { NULL = true };
static readonly AttributeValue NullValue = new AttributeValue { NULL = true };

public static Dictionary<string, AttributeValue> ToExpressionAttributeValues(this IPocoDynamo db, Dictionary<string, object> args)
{
Expand All @@ -207,19 +207,20 @@ public static List<T> QueryInto<T>(this IPocoDynamo db, QueryExpression request,
? arg.Key
: ":" + arg.Key;

if (arg.Value != null)
{
var argType = arg.Value.GetType();
var dbType = db.Converters.GetFieldType(argType);

attrValues[key] = db.Converters.ToAttributeValue(db, argType, dbType, arg.Value);
}
else
{
attrValues[key] = NullValue;
}
attrValues[key] = ToAttributeValue(db, arg.Value);
}
return attrValues;
}

internal static AttributeValue ToAttributeValue(this IPocoDynamo db, object value)
{
if (value == null)
return NullValue;

var argType = value.GetType();
var dbType = db.Converters.GetFieldType(argType);

return db.Converters.ToAttributeValue(db, argType, dbType, value);
}
}
}
168 changes: 168 additions & 0 deletions src/ServiceStack.Aws/DynamoDb/UpdateExpression.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;

namespace ServiceStack.Aws.DynamoDb
{
public class UpdateExpression : UpdateItemRequest
{
protected IPocoDynamo Db { get; set; }

protected DynamoMetadataType Table { get; set; }
}

public class UpdateExpression<T> : UpdateExpression
{
public UpdateExpression(IPocoDynamo db)
: this(db, db.GetTableMetadata(typeof(T)), null, null) { }

public UpdateExpression(IPocoDynamo db, DynamoMetadataType table, object hash, object range = null)
{
this.Db = db;
this.Table = table;
this.TableName = this.Table.Name;
this.Key = db.Converters.ToAttributeKeyValue(db, table, hash, range);
this.ReturnValues = ReturnValue.NONE;
}

public UpdateExpression<T> AddConditionExpression(string conditionExpr)
{
if (this.ConditionExpression == null)
this.ConditionExpression = conditionExpr;
else
this.ConditionExpression += " AND " + conditionExpr;

return this;
}

public UpdateExpression<T> Condition(Expression<Func<T, bool>> conditionExpression)
{
var q = PocoDynamoExpression.Create(typeof(T), conditionExpression, paramPrefix: "p");
return Condition(q.FilterExpression, q.Params);
}

public UpdateExpression<T> Condition(string conditionExpression, Dictionary<string, object> args = null)
{
AddConditionExpression(conditionExpression);

if (args != null)
{
Db.ToExpressionAttributeValues(args).Each(x =>
this.ExpressionAttributeValues[x.Key] = x.Value);
}

return this;
}

public UpdateExpression<T> Set(Expression<Func<T>> fn)
{
var args = fn.AssignedValues();

if (args != null)
{
var hasExpr = UpdateExpression != null
&& UpdateExpression.IndexOf("SET", StringComparison.OrdinalIgnoreCase) >= 0;

foreach (var entry in args)
{
if (UpdateExpression == null)
UpdateExpression = "SET ";
else if (!hasExpr)
UpdateExpression += " SET ";
else
UpdateExpression += ",";

hasExpr = true;

var param = ":" + entry.Key;
this.UpdateExpression += GetMemberName(entry.Key) + " = " + param;
this.ExpressionAttributeValues[param] = Db.ToAttributeValue(entry.Value);
}
}

return this;
}

public UpdateExpression<T> Add(Expression<Func<T>> fn)
{
var args = fn.AssignedValues();

if (args != null)
{
var hasExpr = UpdateExpression != null
&& UpdateExpression.IndexOf("ADD", StringComparison.OrdinalIgnoreCase) >= 0;

foreach (var entry in args)
{
if (UpdateExpression == null)
UpdateExpression = "ADD ";
else if (!hasExpr)
UpdateExpression += " ADD ";
else
UpdateExpression += ",";

hasExpr = true;

var param = ":" + entry.Key;
this.UpdateExpression += GetMemberName(entry.Key) + " " + param;
this.ExpressionAttributeValues[param] = Db.ToAttributeValue(entry.Value);
}
}

return this;
}

public UpdateExpression<T> Remove(Func<T, object> fields)
{
var args = fields.ToObjectKeys().ToArraySafe();

if (args != null)
{
var hasExpr = UpdateExpression != null
&& UpdateExpression.IndexOf("REMOVE", StringComparison.OrdinalIgnoreCase) >= 0;

foreach (var arg in args)
{
if (UpdateExpression == null)
UpdateExpression = "REMOVE ";
else if (!hasExpr)
UpdateExpression += " REMOVE ";
else
UpdateExpression += ",";

hasExpr = true;

this.UpdateExpression += GetMemberName(arg);
}
}

return this;
}

public string GetMemberName(string memberName)
{
if (DynamoConfig.IsReservedWord(memberName))
{
var alias = "#" + memberName.Substring(0, 2).ToUpper();
bool aliasExists = false;
foreach (var entry in ExpressionAttributeNames)
{
if (entry.Value == memberName)
return entry.Key;
if (entry.Key == alias)
aliasExists = true;
}

if (aliasExists)
alias += ExpressionAttributeNames.Count;

ExpressionAttributeNames[alias] = memberName;
return alias;
}

return memberName;
}
}
}
1 change: 1 addition & 0 deletions src/ServiceStack.Aws/ServiceStack.Aws.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
<Compile Include="DynamoDb\ScanExpression.cs" />
<Compile Include="DynamoDb\QueryExpression.cs" />
<Compile Include="DynamoDb\DynamoMetadata.cs" />
<Compile Include="DynamoDb\UpdateExpression.cs" />
<Compile Include="FileStorage\IFileStorageProvider.cs" />
<Compile Include="FileStorage\FileSystemObject.cs" />
<Compile Include="DynamoDb\DynamoUtils.cs" />
Expand Down
114 changes: 114 additions & 0 deletions tests/ServiceStack.Aws.DynamoDbTests/PocoDynamoUpdateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,119 @@ public void TypedApi_does_populate_DynamoUpdateItem()
Assert.That(updatedCustomer.PrimaryAddress, Is.EqualTo(customer.PrimaryAddress));
Assert.That(updatedCustomer.Orders, Is.Null);
}

[Test]
public void Can_SET_ADD_REMOVE_with_conditional_expression_using_UpdateExpression()
{
var db = CreatePocoDynamo()
.RegisterTable<Customer>();

db.DeleteTable<Customer>();
db.InitSchema();

var customer = CreateCustomer();
customer.Age = 27;
db.PutItem(customer);

var decrBy = -1;
var q = db.UpdateExpression<Customer>(customer.Id)
.Set(() => new Customer { Nationality = "Australian" })
.Add(() => new Customer { Age = decrBy })
.Remove(x => new { x.Name, x.Orders })
.Condition(x => x.Age == 27);

var succeeded = db.UpdateItem(q);
Assert.That(succeeded);

var updatedCustomer = db.GetItem<Customer>(customer.Id);
Assert.That(updatedCustomer.Age, Is.EqualTo(customer.Age - 1));
Assert.That(updatedCustomer.Name, Is.Null);
Assert.That(updatedCustomer.Nationality, Is.EqualTo("Australian"));
Assert.That(updatedCustomer.PrimaryAddress, Is.EqualTo(customer.PrimaryAddress));
Assert.That(updatedCustomer.Orders, Is.Null);
}

[Test]
public void Can_update_SET_with_conditional_expression()
{
var db = CreatePocoDynamo()
.RegisterTable<Customer>();

db.DeleteTable<Customer>();
db.InitSchema();

var customer = CreateCustomer();
customer.Age = 27;

db.PutItem(customer);

var q = db.UpdateExpression<Customer>(customer.Id)
.Set(() => new Customer { Age = 30 })
.Condition(x => x.Age == 29);

var succeeded = db.UpdateItem(q);
Assert.That(!succeeded);

var latest = db.GetItem<Customer>(customer.Id);
Assert.That(latest.Age, Is.EqualTo(27));

q = db.UpdateExpression<Customer>(customer.Id)
.Set(() => new Customer { Age = 30 })
.Condition(x => x.Age == 27);

succeeded = db.UpdateItem(q);
Assert.That(succeeded);

latest = db.GetItem<Customer>(customer.Id);
Assert.That(latest.Age, Is.EqualTo(30));
}

[Test]
public void Can_ADD_with_UpdateExpression()
{
var db = CreatePocoDynamo()
.RegisterTable<Customer>();

db.DeleteTable<Customer>();
db.InitSchema();

var customer = CreateCustomer();
customer.Age = 27;
db.PutItem(customer);

var decrBy = -1;

var q = db.UpdateExpression<Customer>(customer.Id)
.Add(() => new Customer { Age = decrBy });

var succeeded = db.UpdateItem(q);
Assert.That(succeeded);

var latest = db.GetItem<Customer>(customer.Id);
Assert.That(latest.Age, Is.EqualTo(26));
}

[Test]
public void Can_REMOVE_with_UpdateExpression()
{
var db = CreatePocoDynamo()
.RegisterTable<Customer>();

db.DeleteTable<Customer>();
db.InitSchema();

var customer = CreateCustomer();
db.PutItem(customer);

var q = db.UpdateExpression<Customer>(customer.Id)
.Remove(x => new { x.Name, x.Orders });

var succeeded = db.UpdateItem(q);
Assert.That(succeeded);

var updatedCustomer = db.GetItem<Customer>(customer.Id);
Assert.That(updatedCustomer.Name, Is.Null);
Assert.That(updatedCustomer.Orders, Is.Null);
}
}
}

0 comments on commit 8d48b13

Please sign in to comment.