Создание конвертера XML JSON в функциях Azure
Ваша первая мысль, вероятно, «но почему?», И это справедливо. Поэтому позвольте мне объяснить.
Я - интенсивный пользователь приложений 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/"><root>
<this att="x"><underthis>val</underthis></this><that>
<withval>x</withval><bigval><![CDATA[
something something
]]></bigval></that></root></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, похоже, тоже не повредит.
Наслаждайтесь!