Создание конвертера XML JSON в функциях Azure

Tags: Azure, JSON

Ваша первая мысль, вероятно, «но почему?», И это справедливо. Поэтому позвольте мне объяснить.

Я - интенсивный пользователь приложений Microsoft Flow и Azure Logic, и оба этих предложения имеют действительно хорошую версию в поддержке JSON, но не для XML. Фактически, вы даже не можете разобрать XML на объекты, на которые вы можете ссылаться в любом из них. Итак, введите мое желание захотеть преобразовать XML в JSON, чтобы я мог передать его на шаге Parse JSON и использовать его позже. Некоторые конечные точки, которые я хочу запросить, возвращают только XML. И вот я здесь.

Тем не менее, делать это с помощью Azure Functions было не так просто, как я надеялся, поэтому я поделюсь с вами, дорогой читатель. Давайте начнем.

Создайте проект HTTP Trigger Azure в Visual Studio:

Я рекомендую придерживаться прав доступа к функциям (против анонимного доступа) для этого, потому что этой функцией было бы легко злоупотреблять, если кто-нибудь обнаружит URL.

Как только вы получите это, вот контент, который нужно использовать для создания двух функций: один для преобразования JSON в XML, а другой для преобразования XML в JSON:

[FunctionName("ConvertToJson")]
public static IActionResult RunToJson([HttpTrigger
(AuthorizationLevel.Function, "post", Route = null)]HttpRequest req, TraceWriter log)
{
    if (req.ContentType.IndexOf(@"/xml", 0, System.StringComparison.OrdinalIgnoreCase) == -1)
    {
        return new BadRequestObjectResult(@"Content-Type header must be an XML content type");
    }

    XmlDocument doc = new XmlDocument();
    doc.Load(req.Body);

    return new OkObjectResult(doc);
}

[FunctionName("ConvertToXml")]
public static async Task<HttpResponseMessage> RunToXmlAsync([HttpTrigger
(AuthorizationLevel.Function, "post", Route = null)]HttpRequest req, TraceWriter log)
{
    if (req.ContentType.IndexOf(@"/json", 0, System.StringComparison.OrdinalIgnoreCase) == -1)
    {
        return new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
        {
            Content = new StringContent(@"Content-Type header must be a JSON content type")
        };
    }

    var json = await req.ReadAsStringAsync();
    XmlDocument doc = JsonConvert.DeserializeXmlNode(json);

    StringBuilder output = new StringBuilder();
    using (var sw = new StringWriter(output))
        doc.WriteTo(new XmlTextWriter(sw));

    return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
    {
        Content = new StringContent(output.ToString(), Encoding.Default, @"application/xml"),
    };
}

Распаковка ConvertToJson

В качестве настройки для дальнейшей работы, давайте посмотрим, как просто было получить XML -> JSON.
Давайте рассмотрим некоторые из упражнений, через которые я пришел к приведенному выше XML -> JSON-коду:

if (req.ContentType.IndexOf(@"/xml", 0, System.StringComparison.OrdinalIgnoreCase) == -1)
{
    return new BadRequestObjectResult(@"Content-Type header must be an XML content type");
}

XmlDocument doc = new XmlDocument();
doc.Load(req.Body);

return new OkObjectResult(doc);

Вот мой запрос на тест в Postman:

POST /api/ConvertToJson HTTP/1.1
Host: localhost:7071
Content-Type: application/xml
Cache-Control: no-cache
Postman-Token: a5dc4ca4-b6dd-4193-b590-d15982219da7

<root>
    <this att="x">
        <underthis>val</underthis>
    </this>
    <that>
        <withval>x</withval>
        <bigval>
            <![CDATA[
            something something
            ]]>
        </bigval>
    </that>
</root>

А вот что вы получите обратно:

Content-Type →application/json; charset=utf-8
Date →Fri, 25 May 2018 18:01:16 GMT
Server →Kestrel
Transfer-Encoding →chunked

{
    "root": {
        "this": {
            "@att": "x",
            "underthis": "val"
        },
        "that": {
            "withval": "x",
            "bigval": {
                "#cdata-section": "\n\t\t\tsomething something\n\t\t\t"
            }
        }
    }
}

Поскольку функции автоматически берут любой объект, данный OkObjectResult, и запускает его через десериализацию JSON, просто давая ему XmlDocument, полученный в результате LoadXml, дает нам именно то, что мы хотим!

Но это связано с некоторым багажом ...

Распаковка ConvertToXml

Это был еще более странным.

if (req.ContentType.IndexOf(@"/json", 0, System.StringComparison.OrdinalIgnoreCase) == -1)
{
    return new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
    {
        Content = new StringContent(@"Content-Type header must be a JSON content type")
    };
}

var json = await req.ReadAsStringAsync();
XmlDocument doc = JsonConvert.DeserializeXmlNode(json);

StringBuilder output = new StringBuilder();
using (var sw = new StringWriter(output))
    doc.WriteTo(new XmlTextWriter(sw));

return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
    Content = new StringContent(output.ToString(), Encoding.Default, @"application/xml"),
};

Общий подход к получению JSON в XML состоит в том, чтобы взять его и просто сериализовать его в XmlNode, используя конструкции Newtonsoft.Json. Там не о чем волноваться, я сделал это.

Для начала, вот запрос Postman, который мы собираемся отправить в ConvertToXml:

POST /api/ConvertToXml HTTP/1.1
Host: localhost:7071
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: 7e55a73f-1d94-46b2-b93f-e7d1297c0c30

{
    "root": {
        "this": {
            "@att": "x",
            "underthis": "val"
        },
        "that": {
            "withval": "x",
            "bigval": {
                "#cdata-section": "\n\t\t\tsomething something\n\t\t\t"
            }
        }
    }
}

Итак, теперь давайте рассмотрим, почему мы не можем просто взять полученный объект XmlDocument и записать его в OkObjectResult.

Первое, что мы должны изменить, чтобы экспериментировать здесь, - это возвращаемое значение ConvertToXml. Вы заметите, что оно установлено в HttpResponseMessage, который является типом,  используемым, как правило, в функции v1 Azure, а не v2. Подробнее об этом позже, но измените его на IActionResult, чтобы подпись теперь выглядела так:

public static async Task<IActionResult> RunToXmlAsync([HttpTrigger
(AuthorizationLevel.Function, "post", Route = null)]HttpRequest req, TraceWriter log)

Теперь измените тело на это:

if (req.ContentType.IndexOf(@"/json", 0, System.StringComparison.OrdinalIgnoreCase) == -1)
{
    return new BadRequestObjectResult(@"Content-Type header must be an JSON content type");
}

var json = await req.ReadAsStringAsync();
XmlDocument doc = JsonConvert.DeserializeXmlNode(json);

return new OkObjectResult(doc);

и дайте ему ход, только чтобы увидеть, как выходит JSON:

Content-Type →application/json; charset=utf-8
Date →Fri, 25 May 2018 17:44:59 GMT
Server →Kestrel
Transfer-Encoding →chunked

{
    "root": {
        "this": {
            "@att": "x",
            "underthis": "val"
        },
        "that": {
            "withval": "x",
            "bigval": {
                "#cdata-section": "\n\t\t\tsomething something\n\t\t\t"
            }
        }
    }
}

На этом этапе я попытался добавить[Produces(@"application/xml")] к сигнатуре моей функции, но это не имело никакого эффекта. К сожалению, функции не должны уважать эти атрибуты ASP.NET Core (пока?).

Давайте попробуем снова заставить вывод быть в XML с помощью коллекции MediaTypeFormatters:

return new OkObjectResult(doc) 
{ ContentTypes = new Microsoft.AspNetCore.Mvc.Formatters.MediaTypeCollection { @"application/xml" } };

Ничего. На этот раз мы получаем HTTP 406 NOT ACCEPTABLE как результат нашей функции.

ОК. Возьмем XML-документ, напишем его в строку и получим это:

XmlDocument doc = JsonConvert.DeserializeXmlNode(json);

StringBuilder sb = new StringBuilder();
using (var sw = new StringWriter(sb))
    doc.WriteTo(new XmlTextWriter(sw));

return new OkObjectResult(sb.ToString());

Дает нам:

Content-Type →text/plain; charset=utf-8
Date →Fri, 25 May 2018 17:51:16 GMT
Server →Kestrel
Transfer-Encoding →chunked

<root><this att="x"><underthis>val</underthis></this><that><withval>x</withval><bigval><![CDATA[
            something something
            ]]></bigval></that></root>

Близко! Но я действительно хочу, чтобы заголовок Content-Type был точным, черт побери!

Давайте добавим наш  MediaTypeFormatter назад в:

return new OkObjectResult(sb.ToString()) 
{ ContentTypes = new Microsoft.AspNetCore.Mvc.Formatters.MediaTypeCollection { @"application/xml" } };

Дает нам...:

Content-Type →application/xml; charset=utf-8
Date →Fri, 25 May 2018 17:52:58 GMT
Server →Kestrel
Transfer-Encoding →chunked

<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">&lt;root&gt;
        &lt;this att="x"&gt;&lt;underthis&gt;val&lt;/underthis&gt;&lt;/this&gt;&lt;that&gt;
        &lt;withval&gt;x&lt;/withval&gt;&lt;bigval&gt;&lt;![CDATA[
            something something
            ]]&gt;&lt;/bigval&gt;&lt;/that&gt;&lt;/root&gt;</string>

Черт возьми! (этодаже не близко к фактическим словам, которые я произносил в этот момент)

Очевидно, что ASP.NET Core и / или функции делают что-то автоматически под обложками, которые я просто не могу контролировать. Я знал, что ASP.NET MVC отлично справляется с этим видом вещей; возможно, .NET Core просто подталкивает всех нас к миру одного лишь JSON? _ (ツ) _ / ¯

В качестве последнего усилия, я преобразовал эту функцию, чтобы использовать конструкции ASP.NET MVC для ответных сообщений. Начиная с подписи:

public static async Task<HttpResponseMessage> RunToXmlAsync
([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequest req, TraceWriter log)

Затем каждый ответ, который я отправлял обратно:

return new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
{
    Content = new StringContent(@"Content-Type header must be a JSON content type")
};
return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
    Content = new StringContent(output.ToString(), Encoding.Default, @"application/xml"),
};

и вы даже не предполагаете...

Content-Length →149
Content-Type →application/xml; charset=utf-8
Date →Fri, 25 May 2018 17:57:08 GMT
Server →Kestrel

<root>
    <this att="x">
        <underthis>val</underthis>
    </this>
    <that>
        <withval>x</withval>
        <bigval>
            <![CDATA[
            something something
            ]]>
        </bigval>
    </that>
</root>

ТАДАМ! У нас есть тело в XML, и у нас есть набор заголовков, чтобы указать его. Отлично!

Использование одной функции с использованием конструкций ASP.NET Core (ConvertToJson) и одной с использованием конструкций ASP.NET MVC, похоже, тоже не повредит.

Наслаждайтесь!

BC3Tech

No Comments

Add a Comment