Использование фильтров с JsonPath в C#

Я использую JsonPath для C# для запроса некоторых данных JSON. JsonPath не имеет собственного синтаксического анализатора, поэтому согласно Совет Рика Сладки, я использую Json.NET чтобы разобрать мою строку Json на набор вложенных объектов IDictionary, массивов IList и примитивов. Затем я использую JsonPath для его фильтрации (после добавления класса, предложенного в Ответ Рика Сладки).

Для справки, вот часть моего кода, которая на самом деле обрабатывает все это:

// string exampleJsonString defined below
string query = "$.store.book[*].title" // we should get a list of all titles

// step 1: use Json.NET to parse the json string into a JObject
JObject parsedJson = JObject.Parse(exampleJsonString); 

// step 2: use JsonPath with a custom ValueSystem (JsonNetValueSystem)
JsonPathContext context = new JsonPathContext    
        { ValueSystem = new JsonNetValueSystem() }; 

// step 3: get results using JsonPath
List<object> toRet = context.SelectNodes(parsedJson,
        query).Select(node => node.Value).ToList();

Причина, по которой я использовал JsonPath в первую очередь, заключалась в его функциональности фильтра. Вы можете выполнять не только обычные запросы, такие как "$.store.book[*].title" (чтобы получить массив всех заголовков в книжном магазине), но и такие запросы, как "$.store.book[?(@.category == 'fiction')].title" (чтобы получить массив всех заголовков в книжном магазине, категория которых соответствует «художественной литературе»). Мне нужно иметь возможность передавать целые запросы в виде строки, поэтому возможность фильтрации во время запросов чрезвычайно полезна.

К сожалению, у меня возникли проблемы с работой этого фильтра. Я ожидаю, что мне придется внести коррективы либо в класс JsonNetValueSystem (первоначально определенный в вышеупомянутый ответ на переполнение стека) или пространство имен JsonPath (вы можете получить JsonPath.cs из кодовая страница Google JsonPath). Если есть какой-то внешний инструмент или альтернативный механизм синтаксического анализа для Json.NET, который позволил бы мне сохранить фильтрацию JsonPath без необходимости писать слишком много дополнительного кода, это было бы идеально, но я почти уверен, что мне придется изменить либо JsonNetValueSystem или сам JsonPath. (Оба из них довольно легко изменить, поскольку это всего лишь файлы .cs, но я не могу копаться в Json.NET без дополнительной работы.)

На самом деле я не могу понять, где исходный код JsonPath обрабатывает фильтрацию, и не могу понять, почему класс JsonNetValueSystem лишает его этой функциональности. Будем очень признательны за любые советы о том, как добавить возможность фильтрации в строку запроса. Даже если это просто «не связывайтесь с JsonPath, просто измените JsonNetValueSystem» или наоборот.

string exampleJsonString = "{
    "store": {
        "book": [ {
            "category": "reference",
            "author": "Nigel Rees",
            "title": "Sayings of the Century",
            "price": 8.95
        }, {
            "category": "fiction",
            "author": "Evelyn Waugh",
            "title": "Sword of Honour",
            "price": 12.99
        }, {
            "category": "fiction",
            "author": "Herman Melville",
            "title": "Moby Dick",
            "isbn": "0-553-21311-3",
            "price": 8.99
        }, {
            "category": "fiction",
            "author": "J. R. R. Tolkien",
            "title": "The Lord of the Rings",
            "isbn": "0-395-19395-8",
            "price": 22.99
        } ],
        "bicycle": [ {
            "color": "red",
            "price": 19.95,
            "style": [ "city", "hybrid" ]
        }, {
            "color": "blue",
            "price": 59.91,
            "style": [ "downhill", "freeride" ]
        } ]
    }
}"

person firechant    schedule 01.08.2013    source источник
comment
Вы пытались разобрать JSON в объект класса и вместо этого использовать Linq для фильтрации?   -  person Quintium    schedule 01.08.2013
comment
Проблема в том, что я не буду заранее знать структуру JSON. Я не могу настроить, скажем, класс Book с категорией, автором, заголовком и ценой, потому что код предназначен для использования в самых разных базах данных, большинство из которых не будет иметь объектов Book. Так что я каждый раз застреваю, разбирая строку в новый JObject. Я не знаю, как создать собственный класс для каждой новой базы данных. Есть ли один? Если так, то это было бы здорово. Если нет, я думаю, что я все еще застрял с тем, что у меня есть, насколько я могу судить. Я не слишком хорошо знаю Linq, поэтому, если есть способ, это было бы здорово.   -  person firechant    schedule 01.08.2013
comment
Непонятый. Я все еще пытаюсь понять Linq сам. Но кажется, что JSON.Net может предоставить (с некоторой работой) то, что вы ищете. Я предлагаю ознакомиться с документацией здесь: james.newtonking.com/projects/json/help/index.html?topic=html/ Может потребоваться, чтобы вы подключились к JArray и методам IObject, чтобы добраться туда, куда вам нужно.   -  person Quintium    schedule 01.08.2013


Ответы (2)


Когда вы используете выражение сценария в запросе (часть ?(...)), вам необходимо предоставить метод ScriptEvaluator для оценки сценария. К сожалению, они не предоставляют реализацию по умолчанию для версии C#. Вам нужно будет предоставить эту реализацию.

Из коробки это будет не самая тривиальная проблема для решения, вам нужно будет написать интерпретатор. У вас есть несколько вариантов: используйте CodeDOM для компиляции и выполнения скрипта как кода C# (или любого другого языка, который вы предпочитаете), используйте Roslyn для разбора скрипта и оценки, что бы ни работало.

Быстрое и грязное решение для этого конкретного случая состояло бы в том, чтобы сделать что-то подобное в вашем методе оценки скрипта:

object EvaluateScript(string script, object value, string context)
{
    if (script == "@.category == 'fiction'")
    {
        var obj = value as JObject;
        return (string)obj["category"] == "fiction";
    }
    return null;
}

Вот еще одно решение, которое использует IronPython для оценки скрипта.

public class IronPythonScriptEvaluator
{
    private Lazy<ScriptEngine> engine = new Lazy<ScriptEngine>(() => Python.CreateEngine());
    private ScriptEngine Engine { get { return engine.Value; } }

    private const string ItemName = "_IronPythonScriptEvaluator_item";

    public object EvaluateScript(string script, object value, string context)
    {
        var cleanScript = CleanupScript(script);
        var scope = Engine.CreateScope();
        scope.SetVariable(ItemName, value);
        return Engine.Execute<bool>(cleanScript, scope);
    }

    private string CleanupScript(string script)
    {
        return Regex.Replace(script, @"@", ItemName);
    }
}
person Jeff Mercado    schedule 02.08.2013
comment
Ага, похоже, это то, что мне придется сделать. Спасибо, что указали мне правильное направление. :) Я опубликую то, что у меня есть, когда закончу. - person firechant; 03.08.2013
comment
Если у вас есть доступ к динамическому языку, такому как IronPython, вы можете использовать его для оценки своего сценария. Я включил пример того, как вы его используете. - person Jeff Mercado; 03.08.2013
comment
Где документация по этому ScriptEvaluator? - person The Oddler; 09.03.2016
comment
@TheOddler: Насколько я знаю, нет никакой документации по реализации C# (или любой другой реализации PHP, доступной на сайте). Вам нужно будет посмотреть на источник, чтобы понять, что ему нужно и что он пытается сделать. Это не самый сложный фрагмент кода, поэтому он должен быть довольно простым. - person Jeff Mercado; 10.03.2016

Другое решение — использовать вместо этого Manatee.Json. Он имеет собственную реализацию JSONPath и встроенный парсер (вместе со схемой и сериализатором). Более того, вам не требуется представлять путь в виде строки. Manatee.Json имеет удобный интерфейс, который можно использовать для создания путей, включая поддержку выражений.

Чтобы представить $.store.book[*].title, вам нужно

var path = JsonPathWith.Name("store")
                       .Name("book")
                       .Array()
                       .Name("title");

Для вашего примера $.store.book[?(@.category == 'fiction')].title вы должны использовать

var path = JsonPathWith.Name("store")
                       .Name("book")
                       .Array(jv => jv.Name("category") == "fiction")
                       .Name("title");

Более того, в этих выражениях также имеется ограниченная поддержка полей.

Если ваш источник пути является строкой, Manatee.Json также выполняет синтаксический анализ пути!

person gregsdennis    schedule 05.10.2015