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

[5.0.1] Query: Don't terminate translation if indexer method does not bind to indexer property #23420

Merged
merged 1 commit into from
Nov 24, 2020
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 @@ -465,7 +465,13 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
// EF Indexer property
if (methodCallExpression.TryGetIndexerArguments(_model, out source, out propertyName))
{
return TryBindMember(Visit(source), MemberIdentity.Create(propertyName));
var result = TryBindMember(Visit(source), MemberIdentity.Create(propertyName));
var useOldBehavior = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue23410", out var enabled) && enabled;
if (result != null
|| useOldBehavior)
{
return result;
}
}

// GroupBy Aggregate case
Expand Down
166 changes: 165 additions & 1 deletion test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@
using Microsoft.EntityFrameworkCore.Diagnostics.Internal;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.EntityFrameworkCore.TestUtilities;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using NetTopologySuite.Geometries;
using Newtonsoft.Json.Linq;
using Xunit;
using Xunit.Abstractions;

Expand Down Expand Up @@ -9308,7 +9311,7 @@ public virtual void Can_query_point_with_buffered_data_reader()
@"SELECT TOP(1) [l].[Id], [l].[Name], [l].[Address_County], [l].[Address_Line1], [l].[Address_Line2], [l].[Address_Point], [l].[Address_Postcode], [l].[Address_Town]
FROM [Locations] AS [l]
WHERE [l].[Name] = N'My Location'" });
}
}

[Owned]
private class Address23282
Expand Down Expand Up @@ -9379,6 +9382,167 @@ public MyContext23282(DbContextOptions options)
#endregion
#region Issue23410
// TODO: Remove when JSON is first class. See issue#4021
[ConditionalFact]
public virtual void Method_call_translators_are_invoked_for_indexer_if_not_indexer_property()
{
var (options, testSqlLoggerFactory) = CreateOptions23410();
using var context = new MyContext23410(options);
var testUser = context.Blogs.FirstOrDefault(x => x.JObject["Author"].Value<string>() == "Maumar");

Assert.NotNull(testUser);

testSqlLoggerFactory.AssertBaseline(
new[] {
@"SELECT TOP(1) [b].[Id], [b].[JObject], [b].[Name]
FROM [Blogs] AS [b]
WHERE JSON_VALUE([b].[JObject], '$.Author') = N'Maumar'" });
}

private class Blog23410
{
public int Id { get; set; }

public string Name { get; set; }
public JObject JObject { get; set; }
}

private class MyContext23410 : DbContext
{
public DbSet<Blog23410> Blogs { get; set; }

public MyContext23410(DbContextOptions options)
: base(options)
{
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog23410>().Property(e => e.JObject).HasConversion(
e => e.ToString(),
e => JObject.Parse(e));
}
}

private class JsonMethodCallTranslatorPlugin : IMethodCallTranslatorPlugin
{
public JsonMethodCallTranslatorPlugin(ISqlExpressionFactory sqlExpressionFactory)
{
Translators = new IMethodCallTranslator[]
{
new JsonIndexerMethodTranslator(sqlExpressionFactory),
new JsonValueMethodTranslator(sqlExpressionFactory)
};
}

public IEnumerable<IMethodCallTranslator> Translators { get; }
}

private class JsonValueMethodTranslator : IMethodCallTranslator
{
private readonly ISqlExpressionFactory _sqlExpressionFactory;

public JsonValueMethodTranslator(ISqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
}

public SqlExpression Translate(
SqlExpression instance,
MethodInfo method,
IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
if (method.IsGenericMethod
&& method.DeclaringType == typeof(Newtonsoft.Json.Linq.Extensions)
&& method.Name == "Value"
&& arguments.Count == 1
&& arguments[0] is SqlFunctionExpression sqlFunctionExpression)
{
return _sqlExpressionFactory.Function(
sqlFunctionExpression.Name,
sqlFunctionExpression.Arguments,
sqlFunctionExpression.IsNullable,
sqlFunctionExpression.ArgumentsPropagateNullability,
method.ReturnType);
}

return null;
}
}

private class JsonIndexerMethodTranslator : IMethodCallTranslator
{
private readonly MethodInfo _indexerMethod = typeof(JObject).GetRuntimeMethod("get_Item", new[] { typeof(string) });

private readonly ISqlExpressionFactory _sqlExpressionFactory;

public JsonIndexerMethodTranslator(ISqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
}

public SqlExpression Translate(
SqlExpression instance,
MethodInfo method,
IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
if (Equals(_indexerMethod, method))
{
return _sqlExpressionFactory.Function(
"JSON_VALUE",
new[] {
instance,
_sqlExpressionFactory.Fragment($"'$.{((SqlConstantExpression)arguments[0]).Value}'")
},
nullable: true,
argumentsPropagateNullability: new[] { true, false },
_indexerMethod.ReturnType);
}

return null;
}
}

private (DbContextOptions, TestSqlLoggerFactory) CreateOptions23410()
{
var testStore = SqlServerTestStore.CreateInitialized("QueryBugsTest");
var testSqlLoggerFactory = new TestSqlLoggerFactory();
var serviceCollection = new ServiceCollection()
.AddSingleton<ILoggerFactory>(testSqlLoggerFactory)
.AddEntityFrameworkSqlServer();
serviceCollection.TryAddEnumerable(new ServiceDescriptor(
typeof(IMethodCallTranslatorPlugin), typeof(JsonMethodCallTranslatorPlugin), ServiceLifetime.Singleton));
var serviceProvider = serviceCollection.BuildServiceProvider();

var optionsBuilder = Fixture.AddOptions(testStore.AddProviderOptions(new DbContextOptionsBuilder()))
.EnableDetailedErrors()
.UseInternalServiceProvider(serviceProvider)
.EnableServiceProviderCaching(false);

var context = new MyContext23410(optionsBuilder.Options);
if (context.Database.EnsureCreatedResiliently())
{
context.Blogs.Add(new Blog23410
{
Name = "My Location",
JObject = JObject.Parse(@"{ ""Author"": ""Maumar"" }")
});
context.SaveChanges();
}

testSqlLoggerFactory.Clear();

return (optionsBuilder.Options, testSqlLoggerFactory);
}

#endregion

private DbContextOptions _options;

private SqlServerTestStore CreateTestStore<TContext>(
Expand Down