Skip to content
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,7 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery
var shaperBody = shapedQueryExpression.ShaperExpression;
shaperBody = new BsonDocumentInjectingExpressionVisitor().Visit(shaperBody);
shaperBody = InjectEntityMaterializers(shaperBody);
shaperBody = new MongoProjectionBindingRemovingExpressionVisitor(
rootEntityType, mongoQueryExpression, bsonDocParameter, trackQueryResults)
shaperBody = new MongoProjectionBindingRemovingExpressionVisitor(mongoQueryExpression, bsonDocParameter, trackQueryResults)
.Visit(shaperBody);

var shaperLambda = Expression.Lambda(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ private static IBsonSerializer GetCollectionSerializer(Type type, IBsonSerialize
return CreateGenericSerializer(typeof(EnumerableInterfaceImplementerSerializer<,>), [type, itemType], childSerializer);
}

internal static BsonSerializationInfo GetPropertySerializationInfo(IReadOnlyProperty property)
internal static BsonSerializationInfo GetPropertySerializationInfo(string? alias, IReadOnlyProperty property)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make the alias optional and come after property rather than passing null in some paths.

{
var serializer = CreateTypeSerializer(property);

Expand All @@ -298,12 +298,12 @@ internal static BsonSerializationInfo GetPropertySerializationInfo(IReadOnlyProp
if (binaryVectorType != null)
{
return new BsonSerializationInfo(
property.GetElementName(),
alias ?? property.GetElementName(),
CreateBinaryVectorSerializer(type, binaryVectorType.Value),
serializer.ValueType);
}

return new BsonSerializationInfo(property.GetElementName(), serializer, serializer.ValueType);
return new BsonSerializationInfo(alias ?? property.GetElementName(), serializer, serializer.ValueType);
}

private static IBsonSerializer CreateBinaryVectorSerializer(Type type, BinaryVectorDataType binaryVectorDataType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public bool TryGetMemberSerializationInfo(string memberName, out BsonSerializati
var property = _entityType.FindProperty(memberName);
if (property != null)
{
serializationInfo = BsonSerializerFactory.GetPropertySerializationInfo(property);
serializationInfo = BsonSerializerFactory.GetPropertySerializationInfo(null, property);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then this won't need to change. No alias, no argument.

return true;
}

Expand Down
96 changes: 37 additions & 59 deletions src/MongoDB.EntityFrameworkCore/Storage/BsonBinding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
*/

using System;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.EntityFrameworkCore.Extensions;
using MongoDB.EntityFrameworkCore.Serializers;

namespace MongoDB.EntityFrameworkCore.Storage;
Expand All @@ -34,60 +35,40 @@ internal static class BsonBinding
/// <summary>
/// Create the expression which will obtain the value or intermediate value required by the shaper.
/// </summary>
/// <param name="bsonDocExpression">The expression to obtain the current <see cref="BsonDocument"/>.</param>
/// <param name="name">The name of the field in the document that contains the desired value.</param>
/// <param name="required">
/// <see langword="true"/> if the field is required to be present in the document,
/// <see langword="false"/> if it is optional.
/// </param>
/// <param name="mappedType">What <see cref="Type"/> to the value is to be treated as.</param>
/// <param name="declaredType">The <see cref="ITypeBase"/> the value will belong to in order to obtaining additional metadata.</param>
/// <param name="documentExpression">The expression to obtain the current <see cref="BsonDocument"/>.</param>
/// <param name="alias">The name of the field in the document that contains the desired value.</param>
/// <param name="propertyBase">The <see cref="INavigation"/> or <see cref="IProperty"/> mapping to the field.</param>
/// <returns>A compilable expression the shaper can use to obtain this value from a <see cref="BsonDocument"/>.</returns>
/// <exception cref="InvalidOperationException">If we can't find anything mapped to this name.</exception>
public static Expression CreateGetValueExpression(
Expression bsonDocExpression,
string? name,
bool required,
Type mappedType,
ITypeBase declaredType)
Expression documentExpression,
string? alias,
IPropertyBase? propertyBase = null)
{
if (name is null)
if (propertyBase is null && alias is null)
{
return bsonDocExpression;
return documentExpression;
}

if (mappedType == typeof(BsonArray))
if (propertyBase is IProperty property)
{
return CreateGetBsonArray(bsonDocExpression, name);
return CreateGetPropertyValue(documentExpression, alias, property);
}

if (mappedType == typeof(BsonDocument))
{
return CreateGetBsonDocument(bsonDocExpression, name, required, declaredType);
}
Debug.Assert(propertyBase is INavigationBase,
$"Not a property and not a navigation, but a {propertyBase.GetType().ShortDisplayName()}");

var targetProperty = declaredType.FindProperty(name);
if (targetProperty != null)
{
return CreateGetPropertyValue(bsonDocExpression, Expression.Constant(targetProperty),
targetProperty.IsNullable ? mappedType.MakeNullable() : mappedType);
}

if (declaredType is IEntityType entityType)
{
var navigationProperty = entityType.FindNavigation(name);
if (navigationProperty != null)
{
var fieldName = navigationProperty.TargetEntityType.GetContainingElementName()!;
return CreateGetElementValue(bsonDocExpression, fieldName, mappedType);
}
}

throw new InvalidOperationException(CoreStrings.PropertyNotFound(name, declaredType.DisplayName()));
var navigationBase = (INavigationBase)propertyBase!;
return navigationBase.IsCollection
? CreateGetBsonArray(documentExpression, alias, navigationBase)
: CreateGetBsonDocument(documentExpression, alias, navigationBase);
}

private static MethodCallExpression CreateGetBsonArray(Expression bsonDocExpression, string name)
=> Expression.Call(null, GetBsonArrayMethodInfo, bsonDocExpression, Expression.Constant(name));
private static MethodCallExpression CreateGetBsonArray(Expression documentExpression, string? alias, INavigationBase navigation)
=> Expression.Call(
GetBsonArrayMethodInfo,
documentExpression,
Expression.Constant(alias ?? navigation.Name, typeof(string)));

private static readonly MethodInfo GetBsonArrayMethodInfo
= typeof(BsonBinding).GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
Expand All @@ -107,10 +88,10 @@ private static readonly MethodInfo GetBsonArrayMethodInfo
}

private static MethodCallExpression CreateGetBsonDocument(
Expression bsonDocExpression, string name, bool required, ITypeBase declaredType)
=> Expression.Call(null, GetBsonDocumentMethodInfo, bsonDocExpression, Expression.Constant(name),
Expression.Constant(required),
Expression.Constant(declaredType));
Expression documentExpression, string? alias, INavigationBase navigationBase)
=> Expression.Call(null, GetBsonDocumentMethodInfo, documentExpression, Expression.Constant(alias ?? navigationBase.Name),
Expression.Constant(navigationBase is INavigation { ForeignKey.IsRequiredDependent: true }),
Expression.Constant(navigationBase.DeclaringEntityType));

private static readonly MethodInfo GetBsonDocumentMethodInfo
= typeof(BsonBinding).GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
Expand All @@ -129,23 +110,20 @@ private static readonly MethodInfo GetBsonDocumentMethodInfo
}

private static MethodCallExpression
CreateGetPropertyValue(Expression bsonDocExpression, Expression propertyExpression, Type resultType) =>
Expression.Call(null, GetPropertyValueMethodInfo.MakeGenericMethod(resultType), bsonDocExpression, propertyExpression);

private static MethodCallExpression CreateGetElementValue(Expression bsonDocExpression, string name, Type type) =>
Expression.Call(null, GetElementValueMethodInfo.MakeGenericMethod(type), bsonDocExpression, Expression.Constant(name));
CreateGetPropertyValue(Expression documentExpression, string? alias, IProperty property)
=> Expression.Call(
GetPropertyValueMethodInfo.MakeGenericMethod(property.IsNullable ? property.ClrType.MakeNullable() : property.ClrType),
documentExpression,
Expression.Constant(alias ?? property.GetElementName(), typeof(string)),
Expression.Constant(property));

private static readonly MethodInfo GetPropertyValueMethodInfo
= typeof(BsonBinding).GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
.Single(mi => mi.Name == nameof(GetPropertyValue));

private static readonly MethodInfo GetElementValueMethodInfo
= typeof(BsonBinding).GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
.Single(mi => mi.Name == nameof(GetElementValue));

internal static T? GetPropertyValue<T>(BsonDocument document, IReadOnlyProperty property)
internal static T? GetPropertyValue<T>(BsonDocument document, string? alias, IReadOnlyProperty property)
{
var serializationInfo = BsonSerializerFactory.GetPropertySerializationInfo(property);
var serializationInfo = BsonSerializerFactory.GetPropertySerializationInfo(alias, property);
if (TryReadElementValue(document, serializationInfo, out T? value))
{
if (value == null && !property.IsNullable)
Expand All @@ -159,7 +137,7 @@ private static readonly MethodInfo GetElementValueMethodInfo

if (property.IsNullable) return default;

throw new InvalidOperationException($"Document element is missing for required non-nullable property '{property.Name}'.");
throw new InvalidOperationException($"Document element is missing for required non-nullable property '{alias ?? property.Name}'.");
}

internal static T? GetElementValue<T>(BsonDocument document, string elementName)
Expand Down
2 changes: 1 addition & 1 deletion src/MongoDB.EntityFrameworkCore/Storage/MongoUpdate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ internal static void WriteNonKeyProperties(IBsonWriter writer, IUpdateEntry entr

private static void WriteProperty(IBsonWriter writer, object? value, IProperty property)
{
var serializationInfo = BsonSerializerFactory.GetPropertySerializationInfo(property);
var serializationInfo = BsonSerializerFactory.GetPropertySerializationInfo(null, property);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

writer.WriteName(serializationInfo.ElementPath?.Last() ?? serializationInfo.ElementName);
var root = BsonSerializationContext.CreateRoot(writer);
serializationInfo.Serializer.Serialize(root, value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2384,43 +2384,46 @@ public override async Task Query_expression_with_to_string_and_contains(bool asy

public override async Task Select_expression_long_to_string(bool async)
{
// Fails: Client eval in final projection EF-250
Assert.Contains(
"The property 'Order.ShipName' could not be found.",
(await Assert.ThrowsAsync<InvalidOperationException>(() => base.Select_expression_long_to_string(async))).Message);
await base.Select_expression_long_to_string(async);

AssertMql(
"""
Orders.{ "$match" : { "OrderDate" : { "$ne" : null } } }, { "$project" : { "ShipName" : { "$toString" : { "$toLong" : "$_id" } }, "_id" : 0 } }
"""
);
}

public override async Task Select_expression_int_to_string(bool async)
{
// Fails: Client eval in final projection EF-250
Assert.Contains(
"The property 'Order.ShipName' could not be found.",
(await Assert.ThrowsAsync<InvalidOperationException>(() => base.Select_expression_int_to_string(async))).Message);
await base.Select_expression_int_to_string(async);

AssertMql(
);
"""
Orders.{ "$match" : { "OrderDate" : { "$ne" : null } } }, { "$project" : { "ShipName" : { "$toString" : "$_id" }, "_id" : 0 } }
"""
);
}

public override async Task ToString_with_formatter_is_evaluated_on_the_client(bool async)
{
// Fails: Client eval in final projection EF-250
Assert.Contains(
"The property 'Order.ShipName' could not be found.",
(await Assert.ThrowsAsync<InvalidOperationException>(() => base.ToString_with_formatter_is_evaluated_on_the_client(async))).Message);
"Expression not supported: o.OrderID.ToString(\"X\")",
(await Assert.ThrowsAsync<ExpressionNotSupportedException>(() => base.ToString_with_formatter_is_evaluated_on_the_client(async))).Message);

AssertMql(
);
"""
Orders.
"""
);
}

public override async Task Select_expression_other_to_string(bool async)
{
// Fails: Client eval in final projection EF-250
Assert.Contains(
"The property 'Order.ShipName' could not be found.",
(await Assert.ThrowsAsync<InvalidOperationException>(() => base.Select_expression_other_to_string(async))).Message);
"cannot be called with instance of type",
(await Assert.ThrowsAsync<ArgumentException>(() => base.Select_expression_other_to_string(async))).Message);

AssertMql(
);
Expand Down Expand Up @@ -2507,7 +2510,7 @@ public override async Task Select_expression_date_add_milliseconds_large_number_
{
// Fails: Projections issue EF-76
Assert.Contains(
"Rewriting child expression",
"No coercion operator is defined",
(await Assert.ThrowsAsync<InvalidOperationException>(() => base.Select_expression_date_add_milliseconds_large_number_divided(async))).Message);

AssertMql(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public void Read_property_value()
var property = entity.GetProperty(nameof(TestEntity.IntProperty));
var document = BsonDocument.Parse("{ IntProperty: 12 }");

var value = BsonBinding.GetPropertyValue<int>(document, property);
var value = BsonBinding.GetPropertyValue<int>(document, null, property);

Assert.Equal(12, value);
}
Expand All @@ -81,7 +81,7 @@ public void Read_missing_property_throws()
var property = entity.GetProperty(nameof(TestEntity.IntProperty));
var document = BsonDocument.Parse("{ property: 12 }");

var ex = Assert.Throws<InvalidOperationException>(() => BsonBinding.GetPropertyValue<int>(document, property));
var ex = Assert.Throws<InvalidOperationException>(() => BsonBinding.GetPropertyValue<int>(document, null, property));
Assert.Contains("IntProperty", ex.Message);
}

Expand All @@ -97,7 +97,7 @@ public void Read_missing_nullable_property_returns_default()
var property = entity.GetProperty(nameof(TestEntity.NullableProperty));

var document = BsonDocument.Parse("{ somevalue: 12 }");
var value = BsonBinding.GetPropertyValue<int?>(document, property);
var value = BsonBinding.GetPropertyValue<int?>(document, null, property);

Assert.Null(value);
}
Expand Down