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
3 changes: 2 additions & 1 deletion ChangeLog/7.2.2-dev.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[main] Query.CreateDelayedQuery(key, Func<IOrderedQueryable<TElement>>) applies external key instead of default computed, as it suppose to
[main] QueryEndpoint.SingleAsync()/SingleOrDefaultAsync() get overloads that can recieve one key value as parameter without need to create array explicitly
[main] QueryEndpoint.SingleAsync()/SingleOrDefaultAsync() get overloads that can recieve one key value as parameter without need to create array explicitly
[main] Support for C#14+ optimization that applies ReadOnlySpan<T>.Contains() extension instead of IEnumerable<T>.Contains() one to arrays
26 changes: 26 additions & 0 deletions Orm/Xtensive.Orm/Core/Extensions/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,32 @@ public static Expression StripMemberAccessChain(this Expression expression)
return expression;
}

/// <summary>
/// Strips implicit cast operators calls.
/// </summary>
/// <param name="expression">Expression to process.</param>
/// <returns><paramref name="expression"/> with chan of implicit casts removed (if any).</returns>
public static Expression StripImplicitCast(this Expression expression)
{
while (expression.NodeType is ExpressionType.Call or ExpressionType.Convert or ExpressionType.ConvertChecked) {
if (expression.NodeType == ExpressionType.Call) {
var mc = expression as MethodCallExpression;
if (mc.Method.Name.Equals(WellKnown.Operator.Implicit, StringComparison.Ordinal))
expression = mc.Arguments[0];
else
break;
}
else {
var unary = expression as UnaryExpression;
if (unary.Method is not null && unary.Method.Name.Equals(WellKnown.Operator.Implicit, StringComparison.Ordinal))
expression = unary.Operand;
else
break;
}
}
return expression;
}

#endregion
}
}
67 changes: 67 additions & 0 deletions Orm/Xtensive.Orm/Linq/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
Expand All @@ -27,6 +28,10 @@ public static class ExpressionExtensions

private static readonly Func<Type, MethodInfo> TupleValueAccessorFactory;

private static readonly Type MemoryExtensionsType = typeof(MemoryExtensions);
private static readonly int[] MemoryExtensionsContainsMethodTokens;
private static readonly MethodInfo EnumerableContains;

///<summary>
/// Makes <see cref="Tuples.Tuple.GetValueOrDefault{T}"/> method call.
///</summary>
Expand Down Expand Up @@ -72,6 +77,46 @@ public static Expression LiftToNullable(this Expression expression) =>
/// <returns>Expression tree that wraps <paramref name="expression"/>.</returns>
public static ExpressionTree ToExpressionTree(this Expression expression) => new ExpressionTree(expression);

/// <summary>
/// Transforms <see cref="MemoryExtensions.Contains{T}(ReadOnlySpan{T}, T)"/> applied call into <see cref="Enumerable.Contains{TSource}(IEnumerable{TSource}, TSource)"/>
/// if detected.
/// </summary>
/// <param name="mc">Possible candidate for transformation.</param>
/// <returns>New instance of expression, if transformation was required, otherwise, the same expression.</returns>
public static MethodCallExpression TryTransformToOldFashionContains(this MethodCallExpression mc)
{
if (mc.Method.DeclaringType == MemoryExtensionsType) {
var genericMethod = mc.Method.GetGenericMethodDefinition();
if (MemoryExtensionsContainsMethodTokens.Contains(genericMethod.MetadataToken)) {
var arguments = mc.Arguments;

Type elementType;
Expression[] newArguments;

if (arguments[0] is MethodCallExpression mcInner && mcInner.Method.Name.Equals(WellKnown.Operator.Implicit, StringComparison.Ordinal)) {
var wrappedArray = mcInner.Arguments[0];
elementType = wrappedArray.Type.GetElementType();
newArguments = new[] { wrappedArray, arguments[1] };
}
else if (arguments[0] is UnaryExpression uInner
&& uInner.Method is not null
&& uInner.Method.Name.Equals(WellKnown.Operator.Implicit, StringComparison.Ordinal)) {

elementType = uInner.Operand.Type.GetElementType();
newArguments = new[] { uInner.Operand, arguments[1] };
}
else {
return mc;
}

var genericContains = EnumerableContains.CachedMakeGenericMethod(elementType);
var replacement = Expression.Call(genericContains, newArguments);
return replacement;
}
return mc;
}
return mc;
}

// Type initializer

Expand All @@ -80,6 +125,28 @@ static ExpressionExtensions()
var tupleGenericAccessor = WellKnownOrmTypes.Tuple.GetMethods()
.Single(mi => mi.Name == nameof(Tuple.GetValueOrDefault) && mi.IsGenericMethod);
TupleValueAccessorFactory = type => tupleGenericAccessor.CachedMakeGenericMethod(type);

var genericReadOnlySpan = typeof(ReadOnlySpan<>);
var genericSpan = typeof(Span<>);

var filteredByNameItems = MemoryExtensionsType.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Where(m => m.Name.Equals(nameof(System.MemoryExtensions.Contains), StringComparison.OrdinalIgnoreCase));

var candiates = new List<int>();

foreach (var method in filteredByNameItems) {
var parameters = method.GetParameters();
var genericDef = parameters[0].ParameterType.GetGenericTypeDefinition();
if (genericDef == genericReadOnlySpan) {
if (parameters.Length == 2 || parameters.Length == 3)
candiates.Add(method.MetadataToken);
}
else if (genericDef == genericSpan && parameters.Length == 2) {
candiates.Add(method.MetadataToken);
}
}
MemoryExtensionsContainsMethodTokens = candiates.ToArray();
EnumerableContains = typeof(System.Linq.Enumerable).GetMethodEx(nameof(System.Linq.Enumerable.Contains), BindingFlags.Public | BindingFlags.Static, new string[1], new object[2]);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ protected override Expression VisitMethodCall(MethodCallExpression mc)
return base.VisitMethodCall(mc);

var method = mc.Method;
if (method.Name=="Contains" && mc.Object!=null) {
if (method.Name == Reflection.WellKnown.Queryable.Contains && mc.Object!=null) {
var elementType = GetEntitySetElementType(mc.Object.Type);
var actualMethod = WellKnownMembers.Queryable.Contains.CachedMakeGenericMethod(elementType);
return Expression.Call(actualMethod, Visit(mc.Object), Visit(mc.Arguments[0]));
Expand Down
11 changes: 11 additions & 0 deletions Orm/Xtensive.Orm/Orm/Linq/Translator.Expressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,17 @@ protected override Expression VisitMethodCall(MethodCallExpression mc)
}
}

if (methodDeclaringType == typeof(MemoryExtensions)) {
var parameters = method.GetParameters();

if (methodName.Equals(nameof(MemoryExtensions.Contains), StringComparison.Ordinal)) {
// There might be 2 or 3 arguments.
// In case of three, last one is IEqualityComparer<T> which will probably have default value
// Comparer doesn't matter in context of our queries, so we ignore it
return VisitContains(mc.Arguments[0].StripImplicitCast(), mc.Arguments[1], false);
}
}


// Process local collections
if (mc.Object.IsLocalCollection(context)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,10 @@ protected override SqlExpression VisitMethodCall(MethodCallExpression mc)
if (mc.AsTupleAccess(activeParameters) != null)
return VisitTupleAccess(mc);

if (mc.Method.Name.Equals(nameof(Enumerable.Contains), StringComparison.Ordinal)) {
// there might be "innovative" implicit cast to ReadOnlySpan inside, which is not supported by expression tree but yet existing
mc = mc.TryTransformToOldFashionContains();
}
var arguments = mc.Arguments.SelectToArray(a => Visit(a));
var mi = mc.Method;

Expand Down
Loading