Skip to content

Commit 7486a25

Browse files
authored
Merge pull request #462 from DataObjects-NET/7.2-memory-extensions-support
MemoryExtensions.Contains support
2 parents 30708a1 + a46bbc1 commit 7486a25

File tree

6 files changed

+111
-2
lines changed

6 files changed

+111
-2
lines changed

ChangeLog/7.2.2-dev.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
[main] Query.CreateDelayedQuery(key, Func<IOrderedQueryable<TElement>>) applies external key instead of default computed, as it suppose to
2-
[main] QueryEndpoint.SingleAsync()/SingleOrDefaultAsync() get overloads that can recieve one key value as parameter without need to create array explicitly
2+
[main] QueryEndpoint.SingleAsync()/SingleOrDefaultAsync() get overloads that can recieve one key value as parameter without need to create array explicitly
3+
[main] Support for C#14+ optimization that applies ReadOnlySpan<T>.Contains() extension instead of IEnumerable<T>.Contains() one to arrays

Orm/Xtensive.Orm/Core/Extensions/ExpressionExtensions.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,32 @@ public static Expression StripMemberAccessChain(this Expression expression)
315315
return expression;
316316
}
317317

318+
/// <summary>
319+
/// Strips implicit cast operators calls.
320+
/// </summary>
321+
/// <param name="expression">Expression to process.</param>
322+
/// <returns><paramref name="expression"/> with chan of implicit casts removed (if any).</returns>
323+
public static Expression StripImplicitCast(this Expression expression)
324+
{
325+
while (expression.NodeType is ExpressionType.Call or ExpressionType.Convert or ExpressionType.ConvertChecked) {
326+
if (expression.NodeType == ExpressionType.Call) {
327+
var mc = expression as MethodCallExpression;
328+
if (mc.Method.Name.Equals(WellKnown.Operator.Implicit, StringComparison.Ordinal))
329+
expression = mc.Arguments[0];
330+
else
331+
break;
332+
}
333+
else {
334+
var unary = expression as UnaryExpression;
335+
if (unary.Method is not null && unary.Method.Name.Equals(WellKnown.Operator.Implicit, StringComparison.Ordinal))
336+
expression = unary.Operand;
337+
else
338+
break;
339+
}
340+
}
341+
return expression;
342+
}
343+
318344
#endregion
319345
}
320346
}

Orm/Xtensive.Orm/Linq/ExpressionExtensions.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
using System;
88
using System.Collections.Concurrent;
9+
using System.Collections.Generic;
910
using System.Linq;
1011
using System.Linq.Expressions;
1112
using System.Reflection;
@@ -27,6 +28,10 @@ public static class ExpressionExtensions
2728

2829
private static readonly Func<Type, MethodInfo> TupleValueAccessorFactory;
2930

31+
private static readonly Type MemoryExtensionsType = typeof(MemoryExtensions);
32+
private static readonly int[] MemoryExtensionsContainsMethodTokens;
33+
private static readonly MethodInfo EnumerableContains;
34+
3035
///<summary>
3136
/// Makes <see cref="Tuples.Tuple.GetValueOrDefault{T}"/> method call.
3237
///</summary>
@@ -72,6 +77,46 @@ public static Expression LiftToNullable(this Expression expression) =>
7277
/// <returns>Expression tree that wraps <paramref name="expression"/>.</returns>
7378
public static ExpressionTree ToExpressionTree(this Expression expression) => new ExpressionTree(expression);
7479

80+
/// <summary>
81+
/// Transforms <see cref="MemoryExtensions.Contains{T}(ReadOnlySpan{T}, T)"/> applied call into <see cref="Enumerable.Contains{TSource}(IEnumerable{TSource}, TSource)"/>
82+
/// if detected.
83+
/// </summary>
84+
/// <param name="mc">Possible candidate for transformation.</param>
85+
/// <returns>New instance of expression, if transformation was required, otherwise, the same expression.</returns>
86+
public static MethodCallExpression TryTransformToOldFashionContains(this MethodCallExpression mc)
87+
{
88+
if (mc.Method.DeclaringType == MemoryExtensionsType) {
89+
var genericMethod = mc.Method.GetGenericMethodDefinition();
90+
if (MemoryExtensionsContainsMethodTokens.Contains(genericMethod.MetadataToken)) {
91+
var arguments = mc.Arguments;
92+
93+
Type elementType;
94+
Expression[] newArguments;
95+
96+
if (arguments[0] is MethodCallExpression mcInner && mcInner.Method.Name.Equals(WellKnown.Operator.Implicit, StringComparison.Ordinal)) {
97+
var wrappedArray = mcInner.Arguments[0];
98+
elementType = wrappedArray.Type.GetElementType();
99+
newArguments = new[] { wrappedArray, arguments[1] };
100+
}
101+
else if (arguments[0] is UnaryExpression uInner
102+
&& uInner.Method is not null
103+
&& uInner.Method.Name.Equals(WellKnown.Operator.Implicit, StringComparison.Ordinal)) {
104+
105+
elementType = uInner.Operand.Type.GetElementType();
106+
newArguments = new[] { uInner.Operand, arguments[1] };
107+
}
108+
else {
109+
return mc;
110+
}
111+
112+
var genericContains = EnumerableContains.CachedMakeGenericMethod(elementType);
113+
var replacement = Expression.Call(genericContains, newArguments);
114+
return replacement;
115+
}
116+
return mc;
117+
}
118+
return mc;
119+
}
75120

76121
// Type initializer
77122

@@ -80,6 +125,28 @@ static ExpressionExtensions()
80125
var tupleGenericAccessor = WellKnownOrmTypes.Tuple.GetMethods()
81126
.Single(mi => mi.Name == nameof(Tuple.GetValueOrDefault) && mi.IsGenericMethod);
82127
TupleValueAccessorFactory = type => tupleGenericAccessor.CachedMakeGenericMethod(type);
128+
129+
var genericReadOnlySpan = typeof(ReadOnlySpan<>);
130+
var genericSpan = typeof(Span<>);
131+
132+
var filteredByNameItems = MemoryExtensionsType.GetMethods(BindingFlags.Public | BindingFlags.Static)
133+
.Where(m => m.Name.Equals(nameof(System.MemoryExtensions.Contains), StringComparison.OrdinalIgnoreCase));
134+
135+
var candiates = new List<int>();
136+
137+
foreach (var method in filteredByNameItems) {
138+
var parameters = method.GetParameters();
139+
var genericDef = parameters[0].ParameterType.GetGenericTypeDefinition();
140+
if (genericDef == genericReadOnlySpan) {
141+
if (parameters.Length == 2 || parameters.Length == 3)
142+
candiates.Add(method.MetadataToken);
143+
}
144+
else if (genericDef == genericSpan && parameters.Length == 2) {
145+
candiates.Add(method.MetadataToken);
146+
}
147+
}
148+
MemoryExtensionsContainsMethodTokens = candiates.ToArray();
149+
EnumerableContains = typeof(System.Linq.Enumerable).GetMethodEx(nameof(System.Linq.Enumerable.Contains), BindingFlags.Public | BindingFlags.Static, new string[1], new object[2]);
83150
}
84151
}
85152
}

Orm/Xtensive.Orm/Orm/Linq/Rewriters/EntitySetAccessRewriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ protected override Expression VisitMethodCall(MethodCallExpression mc)
2626
return base.VisitMethodCall(mc);
2727

2828
var method = mc.Method;
29-
if (method.Name=="Contains" && mc.Object!=null) {
29+
if (method.Name == Reflection.WellKnown.Queryable.Contains && mc.Object!=null) {
3030
var elementType = GetEntitySetElementType(mc.Object.Type);
3131
var actualMethod = WellKnownMembers.Queryable.Contains.CachedMakeGenericMethod(elementType);
3232
return Expression.Call(actualMethod, Visit(mc.Object), Visit(mc.Arguments[0]));

Orm/Xtensive.Orm/Orm/Linq/Translator.Expressions.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,17 @@ protected override Expression VisitMethodCall(MethodCallExpression mc)
530530
}
531531
}
532532

533+
if (methodDeclaringType == typeof(MemoryExtensions)) {
534+
var parameters = method.GetParameters();
535+
536+
if (methodName.Equals(nameof(MemoryExtensions.Contains), StringComparison.Ordinal)) {
537+
// There might be 2 or 3 arguments.
538+
// In case of three, last one is IEqualityComparer<T> which will probably have default value
539+
// Comparer doesn't matter in context of our queries, so we ignore it
540+
return VisitContains(mc.Arguments[0].StripImplicitCast(), mc.Arguments[1], false);
541+
}
542+
}
543+
533544

534545
// Process local collections
535546
if (mc.Object.IsLocalCollection(context)) {

Orm/Xtensive.Orm/Orm/Providers/Expressions/ExpressionProcessor.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,10 @@ protected override SqlExpression VisitMethodCall(MethodCallExpression mc)
412412
if (mc.AsTupleAccess(activeParameters) != null)
413413
return VisitTupleAccess(mc);
414414

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

0 commit comments

Comments
 (0)