ASP MVC .NET で XML を POST として ActionResult に渡す方法
-
12-09-2019 - |
質問
ASP MVC プロジェクトに単純な RESTful API を提供しようとしています。この API のクライアントは制御できません。クライアントは、サーバー側でいくつかのアクションを実行し、アクションの結果を XML で返すために必要な情報を含む XML を POST メソッド経由で渡します。XML を返送することに問題はありませんが、問題は POST 経由で XML を受信することです。JSON の例をいくつか見てきましたが、私はクライアントを制御しないので (私の観点からすると Telnet である可能性もあります)、JSON は機能しないと思います。私は正しいでしょうか?
クライアントがリクエストの本文の一部として正しいフォーム形式を構築するだけで、ASP がメッセージを解析し、データが FormCollection (?param1=value1¶m2=value2& など) として利用できる例を見てきました。ただし、純粋な XML をメッセージ本文の一部として渡したいと考えています。
ご協力いただきありがとうございます、
解決 2
これは、ActionFilterAttribute を使用して実現できます。アクション フィルターは基本的に、アクションの結果の前または後にリクエストと交差します。そこで、POST Action Result のカスタム アクション フィルター属性を構築しました。私がやったことは次のとおりです。
public class RestAPIAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
HttpContextBase httpContext = filterContext.HttpContext;
if (!httpContext.IsPostNotification)
{
throw new InvalidOperationException("Only POST messages allowed on this resource");
}
Stream httpBodyStream = httpContext.Request.InputStream;
if (httpBodyStream.Length > int.MaxValue)
{
throw new ArgumentException("HTTP InputStream too large.");
}
int streamLength = Convert.ToInt32(httpBodyStream.Length);
byte[] byteArray = new byte[streamLength];
const int startAt = 0;
/*
* Copies the stream into a byte array
*/
httpBodyStream.Read(byteArray, startAt, streamLength);
/*
* Convert the byte array into a string
*/
StringBuilder sb = new StringBuilder();
for (int i = 0; i < streamLength; i++)
{
sb.Append(Convert.ToChar(byteArray[i]));
}
string xmlBody = sb.ToString();
//Sends XML Data To Model so it could be available on the ActionResult
base.OnActionExecuting(filterContext);
}
}
次に、コントローラーのアクション結果メソッドで次のようなことを行う必要があります。
[RestAPIAttribute]
public ActionResult MyActionResult()
{
//Gets XML Data From Model and do whatever you want to do with it
}
これが他の人の役に立つことを願っています。もっとエレガントな方法があると思われる場合は、私に知らせてください。
他のヒント
@Freddy - あなたのアプローチを気に入って、ストリームの読み取りを簡素化するために次のコードでそれを改善しました。
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
HttpContextBase httpContext = filterContext.HttpContext;
if (!httpContext.IsPostNotification)
{
throw new InvalidOperationException("Only POST messages allowed on this resource");
}
Stream httpBodyStream = httpContext.Request.InputStream;
if (httpBodyStream.Length > int.MaxValue)
{
throw new ArgumentException("HTTP InputStream too large.");
}
StreamReader reader = new StreamReader(httpBodyStream, Encoding.UTF8);
string xmlBody = reader.ReadToEnd();
reader.Close();
filterContext.ActionParameters["message"] = xmlBody;
// Sends XML Data To Model so it could be available on the ActionResult
base.OnActionExecuting(filterContext);
}
次に、コントローラーで XML に文字列としてアクセスできます。
[RestAPIAttribute]
public ActionResult MyActionResult(string message)
{
}
カスタム値プロバイダー ファクトリを作成できることは知っています。これにより、モデルを保存する前に、投稿時にモデルを検証することもできます。 フィル・ハーク これと同じ概念の JSON バージョンに関するブログ投稿があります。唯一の問題は、これと同じことを XML に実装する方法がわからないことです。
IMO によると、これを達成するための最良の方法は、カスタム値プロバイダーを記述することです。これは、フォーム ディクショナリへのリクエストのマッピングを処理するファクトリです。ValueProviderFactory を継承し、リクエストのタイプが「text/xml」または「application/xml」の場合は、そのリクエストを処理するだけです。
より詳しい情報:
protected override void OnApplicationStarted()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
ValueProviderFactories.Factories.Add(new JsonValueProviderFactory());
ValueProviderFactories.Factories.Add(new XmlValueProviderFactory());
}
XmlValueProviderFactory
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Web.Mvc;
using System.Xml;
using System.Xml.Linq;
public class XmlValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
var deserializedXml = GetDeserializedXml(controllerContext);
if (deserializedXml == null) return null;
var backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
AddToBackingStore(backingStore, string.Empty, deserializedXml.Root);
return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
}
private static void AddToBackingStore(Dictionary<string, object> backingStore, string prefix, XElement xmlDoc)
{
// Check the keys to see if this is an array or an object
var uniqueElements = new List<String>();
var totalElments = 0;
foreach (XElement element in xmlDoc.Elements())
{
if (!uniqueElements.Contains(element.Name.LocalName))
uniqueElements.Add(element.Name.LocalName);
totalElments++;
}
var isArray = (uniqueElements.Count == 1 && totalElments > 1);
// Add the elements to the backing store
var elementCount = 0;
foreach (XElement element in xmlDoc.Elements())
{
if (element.HasElements)
{
if (isArray)
AddToBackingStore(backingStore, MakeArrayKey(prefix, elementCount), element);
else
AddToBackingStore(backingStore, MakePropertyKey(prefix, element.Name.LocalName), element);
}
else
{
backingStore.Add(MakePropertyKey(prefix, element.Name.LocalName), element.Value);
}
elementCount++;
}
}
private static string MakeArrayKey(string prefix, int index)
{
return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
}
private static string MakePropertyKey(string prefix, string propertyName)
{
if (!string.IsNullOrEmpty(prefix))
return prefix + "." + propertyName;
return propertyName;
}
private XDocument GetDeserializedXml(ControllerContext controllerContext)
{
var contentType = controllerContext.HttpContext.Request.ContentType;
if (!contentType.StartsWith("text/xml", StringComparison.OrdinalIgnoreCase) &&
!contentType.StartsWith("application/xml", StringComparison.OrdinalIgnoreCase))
return null;
XDocument xml;
try
{
var xmlReader = new XmlTextReader(controllerContext.HttpContext.Request.InputStream);
xml = XDocument.Load(xmlReader);
}
catch (Exception)
{
return null;
}
if (xml.FirstNode == null)//no xml.
return null;
return xml;
}
}
フォーム投稿で XML を文字列として渡せないのはなぜですか?
例:
public ActionResult SendMeXml(string xml)
{
//Parse into a XDocument or something else if you want, and return whatever you want.
XDocument xmlDocument = XDocument.Parse(xml);
return View();
}
フォーム投稿を作成し、それを 1 つのフォーム フィールドで送信できます。
@Freddy からの回答と @Bowerm からの改善が気に入っています。これは簡潔であり、フォームベースのアクションの形式を維持します。
ただし、IsPostNotification チェックは運用コードでは機能しません。エラー メッセージが示唆しているように、HTTP 動詞はチェックされません。また、コンパイル デバッグ フラグが false に設定されている場合、HTTP 動詞は HTTP コンテキストから削除されます。これについては次のように説明されています。コンパイル デバッグが false の場合、HttpContext.IsPostNotification は false になります
これにより、この問題によるデバッグ ルートの半日が誰かに節約されることを願っています。そのチェックを行わない場合の解決策は次のとおりです。
public class XmlApiAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
HttpContextBase httpContext = filterContext.HttpContext;
// Note: for release code IsPostNotification stripped away, so don't check it!
// https://stackoverflow.com/questions/28877619/httpcontext-ispostnotification-is-false-when-compilation-debug-is-false
Stream httpBodyStream = httpContext.Request.InputStream;
if (httpBodyStream.Length > int.MaxValue)
{
throw new ArgumentException("HTTP InputStream too large.");
}
StreamReader reader = new StreamReader(httpBodyStream, Encoding.UTF8);
string xmlBody = reader.ReadToEnd();
reader.Close();
filterContext.ActionParameters["xmlDoc"] = xmlBody;
// Sends XML Data To Model so it could be available on the ActionResult
base.OnActionExecuting(filterContext);
}
}
...
public class MyXmlController
{ ...
[XmlApiAttribute]
public JsonResult PostXml(string xmlDoc)
{
...
ニース!、
Xml を操作するためにコントローラー メソッドで取得したオブジェクトは何ですか?
私はこの方法を使用しています:
actionFilter で、モデルに以下を設定します。
.
.
string xmlBody = sb.ToString();
filterContext.Controller.ViewData.Model = xmlBody;
そして、私のコントローラーメソッドでは、モデルを次のように取得します。
string xmlUserResult = ViewData.Model as string;
XmlSerializer ser = new XmlSerializer(typeof(UserDTO));
StringReader stringReader = new StringReader(xmlUserResult);
XmlTextReader xmlReader = new XmlTextReader(stringReader);
UserDTO userToUpdate = ser.Deserialize(xmlReader) as UserDTO;
xmlReader.Close();
stringReader.Close();
これは正しい実装ですか?
ありがとう。