I am working on a legacy project which uses SQL Server and stores JSON is some of the columns, which has been upgraded to EF Core 6.
My main issue is that I have an old query which probably ran client-side in previous versions, but now fails at runtime because EF Core 6 doesn't know how to translate it. It needs to use OPENJSON with JSON_VALUE to filter the items server side.
What I am querying is this Items
table, which contains the Sections
column, and this column contains a JSON array:
SELECT TOP 2 * FROM Items
Id Value Sections
1 100 '[{"SectionCode":"Code1","Info":"Something"}, {"SectionCode":"Code2","Info":"Something"}]'
2 100 '[{"SectionCode":"Code3","Info":"Something"}, {"SectionCode":"Code2","Info":"Something"}]'
Item class is defined as follows:
public class Item
{
public int Id { get; set; }
public int Value { get; set; }
public IEnumerable<Section> Sections { get; set; }
}
public class Section
{
public string SectionCode { get; set; }
public string Info { get; set; }
}
To get all items with specific codes in SQL, we can use OPENJSON
and JSON_VALUE
:
SELECT * FROM Items
WHERE EXISTS
(
SELECT 1
FROM OPENJSON(Sections) AS [o]
WHERE JSON_VALUE([o].[Value], '$.SectionCode') IN ('Code1', 'Code2')
)
However I try the same in EF Core:
var items = Context.Items
.Where(x => JsonExtensions
.OpenJson(nameof(x.Sections))
.Any(i => listOfCodes.Contains(JsonExtensions.JsonValue(i.Value, "$.SectionCode")))));
and the OpenJson function is registered like this:
modelBuilder.HasDbFunction(() => JsonExtensions.OpenJson(default));
public class JsonExtensions
{
[DbFunction("OPENJSON", Schema = "", IsBuiltIn = true)]
public static IQueryable<JsonArrayItem> OpenJson(string source)
=> throw new NotSupportedException();
}
[Keyless]
public class JsonArrayItem
{
public int Key { get; set; }
public string Value { get; set; }
public int Type { get; set; }
}
then EF Core translates nameof(x.Sections)
into a string literal and emits:
SELECT * FROM Items
WHERE EXISTS
(
SELECT 1
FROM OPENJSON('Sections') AS [o] -- this should be OPENJSON(Sections)
WHERE JSON_VALUE([o].[Value], '$.SectionCode') IN ('Code1', 'Code2')
)
I wanted to add a custom translation to JsonExtension.OpenJson
so that it passes the value as a constant expression (I do the same thing for JsonValue
), but if I try this:
modelBuilder
.HasDbFunction(typeof(JsonExtensions).GetMethod(nameof(JsonExtensions.OpenJson)))
.HasTranslation(args => new SqlFunctionExpression("OPENJSON", new[]
{
new SqlFragmentExpression((args[0] as SqlConstantExpression).Value as string)
},
true,
new[] { true },
typeof(string),
null
))
.HasParameter("column").Metadata.TypeMapping = stringTypeMapping;
I get the following run-time exception while building the context:
System.InvalidOperationException: 'Cannot set custom translation on the DbFunction 'JsonExtensions.OpenJson' since it is not a scalar function.'
I believe if Sections
was declared as a string in the Item
class, it would work correctly, but the project is already at a pretty late stage at this point so I don't believe this can be changed.
Is there a way to get OPENJSON
to translate the parameter as a column?