使用ASP.NET WebAPI对POST请求进行XML模式验证

时间:2012-08-05 22:27:20

标签: asp.net-mvc xsd asp.net-mvc-4 asp.net-web-api xml-validation

我正在尝试找到一种解决方案来验证POST请求中发送的XML数据是否满足给定的自定义XML架构。

如果我使用ASP.NET Web API提供的XmlMediaTypeFormatter,我可以看到没有可用的架构验证。例如:如果我有模型类型......

public class Order
{
    public string Code { get; set; }
    public int Quantity { get; set; }
}

...以及ApiController ...

中的POST操作
public HttpResponseMessage Post(Order order)
{
    if (ModelState.IsValid)
    {
        // process order...
        // send 200 OK response for example
    }
    else
        // send 400 BadRequest response with ModelState errors in response body
}

...我可以发布以下“错误的”XML数据,但仍会得到200 OK响应:

User-Agent: Fiddler
Host: localhost:45678
Content-Type: application/xml; charset=utf-8

<Order> <Code>12345</Nonsense> </Order>   // malformed XML

或者:

<Order> <CustomerName>12345</CustomerName> </Order>    // invalid property

或者:

<Customer> <Code>12345</Code> </Customer>    // invalid root

或者:

"Hello World"    // no XML at all

等等。

我对请求进行验证的唯一一点是模型绑定:在请求示例1,3和4中,传递到order方法的Postnull,例如2 order.Code属性为null,我可以通过测试order == null或使用Code属性标记[Required]属性来使其无效。我可以在响应中使用400“BadRequest”Http状态代码和响应正文中的验证消息将此验证结果发回。但我无法确切地说出错误是什么,并且无法区分示例1,3和4中的错误XML(没有发布order,这是我能看到的唯一内容) - 例如。

要求Order必须使用特定的自定义XML架构发布,例如xmlns="http://test.org/OrderSchema.xsd",我想验证发布的XML是否对此架构有效,如果不是,在响应中发回模式验证错误。为此,我开始使用自定义MediaTypeFormatter

public class MyXmlMediaTypeFormatter : MediaTypeFormatter
{
    // constructor, CanReadType, CanWriteType, ...

    public override Task<object> ReadFromStreamAsync(Type type, Stream stream,
        HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
    {
        var task = Task.Factory.StartNew(() =>
        {
            using (var streamReader = new StreamReader(stream))
            {
                XDocument document = XDocument.Load(streamReader);
                // TODO: exceptions must the catched here,
                // for example due to malformed XML
                XmlSchemaSet schemaSet = new XmlSchemaSet();
                schemaSet.Add(null, "OrderSchema.xsd");

                var msgs = new List<string>();
                document.Validate(schemaSet, (s, e) => msgs.Add(e.Message));
                // msgs contains now the list of XML schema validation errors
                // I want to send back in the response
                if (msgs.Count == 0)
                {
                    var order = ... // deserialize XML to order
                    return (object)order;
                }
                else
                    // WHAT NOW ?
            }
        });
        return task;
    }
}

只要一切正确,这都可行。

但我不知道如果msgs.Count > 0该怎么办。如何将此验证结果列表“转移”到Post操作,或者如何创建包含这些XML架构验证消息的Http响应?

另外,我不确定自定义MediaTypeFormatter是否是此类XML模式验证的最佳扩展点,以及我的方法是否错误。自定义HttpMessageHandler / DelegatingHandler可能是一个更好的地方吗?或者是否有更简单的开箱即用?

2 个答案:

答案 0 :(得分:5)

如果我这样做,我就不会使用Formatter。格式化程序的主要目标是将线表示转换为CLR类型。在这里,您有一个XML文档,您希望根据一个完全不同的任务的模式进行验证。

我建议创建一个新的MessageHandler来进行验证。派生自DelegatingHandler,如果内容类型为application/xml,则将内容加载到XDocument并进行验证。如果失败,则抛出HttpResponseException。

只需将您的MessageHandler添加到Configuration.MessageHandlers集合中即可设置。

使用派生的XmlMediaTypeFormatter的问题在于,您现在正在ObjectContent代码中嵌入的某个点执行,并且干净地退出可能很棘手。此外,使XmlMediaTypeFormatter更复杂可能不是一个好主意。

我有一个创建MessageHandler的工作。我实际上并没有尝试运行此代码,所以买家要小心。此外,如果你避免阻止调用者,任务的东西会变得很毛茸茸。也许有人会为我清理那些代码,无论如何它都是。

  public class SchemaValidationMessageHandler : DelegatingHandler {

        private XmlSchemaSet _schemaSet;
        public SchemaValidationMessageHandler() {

            _schemaSet = new XmlSchemaSet();
            _schemaSet.Add(null, "OrderSchema.xsd");
        }

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {

            if (request.Content != null && request.Content.Headers.ContentType.MediaType == "application/xml")
            {
                var tcs = new TaskCompletionSource<HttpResponseMessage>();

                var task =  request.Content.LoadIntoBufferAsync()  // I think this is needed so XmlMediaTypeFormatter will still have access to the content
                    .ContinueWith(t => {
                                      request.Content.ReadAsStreamAsync()
                                          .ContinueWith(t2 => {
                                                            var doc = XDocument.Load(t2.Result);
                                                            var msgs = new List<string>();
                                                            doc.Validate(_schemaSet, (s, e) => msgs.Add(e.Message));
                                                            if (msgs.Count > 0) {
                                                                var responseContent = new StringContent(String.Join(Environment.NewLine, msgs.ToArray()));
                                                                 tcs.TrySetException(new HttpResponseException(
                                                                    new HttpResponseMessage(HttpStatusCode.BadRequest) {
                                                                        Content = responseContent
                                                                    }));
                                                            } else {
                                                                tcs.TrySetResult(base.SendAsync(request, cancellationToken).Result);
                                                            }
                                                        });

                                  });
                return tcs.Task;
            } else {
                return base.SendAsync(request, cancellationToken);
            }

        }

答案 1 :(得分:0)

通过反复试验,我找到了一个解决方案(针对问题代码中的WHAT NOW ?占位符):

//...
else
{
    PostOrderErrors errors = new PostOrderErrors
    {
        XmlValidationErrors = msgs
    };
    HttpResponseMessage response = new HttpResponseMessage(
        HttpStatusCode.BadRequest);
    response.Content = new ObjectContent(typeof(PostOrderErrors), errors,
        GlobalConfiguration.Configuration.Formatters.XmlFormatter);
    throw new HttpResponseException(response);
}

...使用这样的响应类:

public class PostOrderErrors
{
    public List<string> XmlValidationErrors { get; set; }
    //...
}

这似乎有效,响应看起来像这样:

HTTP/1.1 400 Bad Request
Content-Type: application/xml; charset=utf-8
<PostOrderErrors xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <XmlValidationErrors>
        <string>Some error text...</string>
        <string>Another error text...</string>
    </XmlValidationErrors>
</PostOrderErrors>