Skip to content

Commit

Permalink
Add full path support for the patch operations (#442)
Browse files Browse the repository at this point in the history
* Add full path support for the patch

* Apply suggestions

* Add backward compatibility
  • Loading branch information
mateuszkumpf authored Jul 1, 2024
1 parent da9fc75 commit c222136
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,34 @@ public PatchOperationBuilder(CosmosPropertyNamingPolicy? cosmosPropertyNamingPol

public IPatchOperationBuilder<TItem> Replace<TValue>(Expression<Func<TItem, TValue>> expression, TValue? value)
{
PropertyInfo property = expression.GetPropertyInfo();
var propertyToReplace = GetPropertyToReplace(property);
_rawPatchOperations.Add(new InternalPatchOperation(property, value, PatchOperationType.Replace));
IReadOnlyList<PropertyInfo> propertyInfos = expression.GetPropertyInfos();
var propertyToReplace = GetPropertyToReplace(propertyInfos);

_rawPatchOperations.Add(new InternalPatchOperation(propertyInfos, value, PatchOperationType.Replace));
_patchOperations.Add(PatchOperation.Replace($"/{propertyToReplace}", value));

return this;
}

private string GetPropertyToReplace(MemberInfo propertyInfo)
private string GetPropertyToReplace(MemberInfo propertyInfo) =>
GetPropertyToReplace([propertyInfo]);

private string GetPropertyToReplace(IEnumerable<MemberInfo> propertyInfos)
{
JsonPropertyAttribute[] attributes =
propertyInfo.GetCustomAttributes<JsonPropertyAttribute>(true).ToArray();
List<string> propertiesNames = [];

foreach (PropertyInfo propertyInfo in propertyInfos)
{
JsonPropertyAttribute[] attributes =
propertyInfo.GetCustomAttributes<JsonPropertyAttribute>(true).ToArray();

var propertyName = attributes.Length is 0
? _namingStrategy.GetPropertyName(propertyInfo.Name, false)
: attributes[0].PropertyName;

propertiesNames.Add(propertyName);
}

return attributes.Length is 0
? _namingStrategy.GetPropertyName(propertyInfo.Name, false)
: attributes[0].PropertyName;
return string.Join("/", propertiesNames);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,67 @@ internal static Expression<Func<T, bool>> Or<T>(

internal static PropertyInfo GetPropertyInfo<TSource, TProperty>(this Expression<Func<TSource, TProperty>> propertyLambda)
{
Type type = typeof(TSource);
var propertyInfos = GetPropertyInfosInternal(propertyLambda);

if (propertyLambda.Body is not MemberExpression member)
PropertyInfo propInfo = propertyInfos[propertyInfos.Count - 1];

ThrowArgumentExceptionIfPropertyIsNotFromSourceType(propertyLambda, propInfo);

return propInfo;
}

internal static IReadOnlyList<PropertyInfo> GetPropertyInfos<TSource, TProperty>(this Expression<Func<TSource, TProperty>> propertyLambda)
{
List<PropertyInfo> propertyInfos = GetPropertyInfosInternal(propertyLambda);

PropertyInfo propInfo = propertyInfos[0];

ThrowArgumentExceptionIfPropertyIsNotFromSourceType(propertyLambda, propInfo);

return propertyInfos;
}

private static List<PropertyInfo> GetPropertyInfosInternal<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda)
{
List<PropertyInfo> propertyInfos = [];

MemberExpression? member = propertyLambda.Body as MemberExpression;

if (member == null)
{
throw new ArgumentException($"Expression '{propertyLambda}' refers to a method, not a property.");
}

while (member != null)
{
if (member.Member is not PropertyInfo propertyInfo)
{
throw new ArgumentException($"Expression '{propertyLambda}' refers to a field, not a property.");
}

if (member.Member is not PropertyInfo propInfo)
throw new ArgumentException($"Expression '{propertyLambda}' refers to a field, not a property.");
propertyInfos.Add(propertyInfo);

member = member.Expression as MemberExpression;
}

propertyInfos.Reverse(); // The properties are added from the leaf to the root, so we reverse to get them in the correct order.

return propertyInfos;
}

private static void ThrowArgumentExceptionIfPropertyIsNotFromSourceType<TSource, TProperty>(
Expression<Func<TSource, TProperty>> propertyLambda,
PropertyInfo propertyInfo)
{
var type = typeof(TSource);

#pragma warning disable IDE0046 // Convert to conditional expression
if (propInfo.ReflectedType != null &&
type != propInfo.ReflectedType &&
!type.IsSubclassOf(propInfo.ReflectedType))
if (propertyInfo.ReflectedType != null &&
type != propertyInfo.ReflectedType &&
!type.IsSubclassOf(propertyInfo.ReflectedType))
{
throw new ArgumentException($"Expression '{propertyLambda}' refers to a property that is not from type {type}.");
}
#pragma warning restore IDE0046 // Convert to conditional expression

return propInfo;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@

namespace Microsoft.Azure.CosmosRepository.Internals;

internal class InternalPatchOperation(PropertyInfo propertyInfo, object? newValue, PatchOperationType type)
internal class InternalPatchOperation(IReadOnlyList<PropertyInfo> propertyInfos, object? newValue, PatchOperationType type)
{
public PatchOperationType Type { get; } = type;
public PropertyInfo PropertyInfo { get; } = propertyInfo;

public IReadOnlyList<PropertyInfo> PropertyInfos { get; } = propertyInfos;

public object? NewValue { get; } = newValue;
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,24 @@ public async ValueTask UpdateAsync(string id,
foreach (InternalPatchOperation internalPatchOperation in
patchOperationBuilder._rawPatchOperations.Where(ipo => ipo.Type is PatchOperationType.Replace))
{
PropertyInfo property = item!.GetType().GetProperty(internalPatchOperation.PropertyInfo.Name)!;
property.SetValue(item, internalPatchOperation.NewValue);
IReadOnlyList<PropertyInfo> propertyInfos = internalPatchOperation.PropertyInfos;
object? currentObject = item;

if (propertyInfos.Count is 0)
{
continue;
}

for (var i = 0; i < propertyInfos.Count; i++)
{
if (i == propertyInfos.Count - 1)
{
propertyInfos[i].SetValue(currentObject, internalPatchOperation.NewValue);
break;
}

currentObject = propertyInfos[i].GetValue(currentObject);
}
}

ConcurrentDictionary<string, string> items = InMemoryStorage.GetDictionary<TItem>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,36 @@ await _rootObjectRepository.UpdateAsync(root.Id, builder =>
Assert.Equal(2, deserialisedItem.NestedObject.Property2);
}

[Fact]
public async Task UpdateAsync_PropertiesInNestedObjectToPatch_UpdatesValues()
{
//Arrange
RootObject root = new()
{
Id = Guid.NewGuid().ToString(),
NestedObject = new NestedObject
{
Property1 = "prop1",
Property2 = 55
}
};

InMemoryStorage.GetDictionary<RootObject>().TryAddAsJson(root.Id, root);

//Act
await _rootObjectRepository.UpdateAsync(
root.Id,
builder =>
builder.Replace(x => x.NestedObject.Property1, "prop2")
.Replace(x => x.NestedObject.Property2, 2));

//Assert
RootObject deserialisedItem =
_rootObjectRepository.DeserializeItem(InMemoryStorage.GetDictionary<RootObject>().First().Value);
Assert.Equal("prop2", deserialisedItem.NestedObject.Property1);
Assert.Equal(2, deserialisedItem.NestedObject.Property2);
}

[Fact]
public async Task PageAsync_PredicateThatDoesNotMatch_ReturnsEmptyList()
{
Expand Down

0 comments on commit c222136

Please sign in to comment.