Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add full path support for the patch operations #442

Merged
Merged
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
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