0

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?

vgru
  • 49,838
  • 16
  • 120
  • 201
  • Why not just `.OpenJson(x.Sections)` instead of `nameof`? For sure in this case you have to store `Sections` as `string`. I think it is only one way that you can use custom `IQueryable` functions with EF Core right now. – Svyatoslav Danyliv Dec 06 '22 at 21:48
  • @SvyatoslavDanyliv thanks, yes I believe if `Sections` were a string in C#, this would work, but unfortunately it's being used all around the code so it's a bit hard to change it now. – vgru Dec 08 '22 at 10:23
  • Make property `Sections` not mapped, and initialize from from new property `SectionsJson`. – Svyatoslav Danyliv Dec 08 '22 at 10:41
  • @SvyatoslavDanyliv does this mean I could get the deserialized Sections as usual, but also be able to use `SectionsJson` with OPENJSON? Can you post an answer please with details and I'll accept it :) – vgru Dec 08 '22 at 11:42

0 Answers0