简体   繁体   English

使用ASP.NET WebAPI对POST请求进行XML架构验证

[英]XML Schema validation for POST requests with ASP.NET WebAPI

I am trying to find a solution to validate if XML data sent in a POST request are fulfilling a given custom XML schema. 我正在尝试找到一种解决方案来验证POST请求中发送的XML数据是否满足给定的自定义XML模式。

If I use the XmlMediaTypeFormatter delivered with ASP.NET Web API I don't have a schema validation available, as far as I can see. 如果我使用随ASP.NET Web API XmlMediaTypeFormatter提供的XmlMediaTypeFormatter ,据我所知,我没有可用的架构验证。 For example: If I have a model type... 例如:如果我有模型类型...

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

...and a POST action in an ApiController ... ...以及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
}

...I can post the following "wrong" XML data and will get a 200 OK response nevertheless: ...我可以发布以下“错误的” XML数据,但是仍然会收到200 OK响应:

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

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

Or: 要么:

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

Or: 要么:

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

Or: 要么:

"Hello World"    // no XML at all

etc., etc. 等等等

The only point where I have a validation of the request is model binding: In request example 1, 3 and 4 the order passed into the Post method is null , in example 2 the order.Code property is null which I could invalidate by testing for order == null or by marking the Code property with a [Required] attribute. 我可以验证请求的唯一点是模型绑定:在请求示例1、3和4中,传递给Post方法的ordernull ,在示例2中, order.Code属性为null ,我可以通过测试来使该属性无效order == null或通过使用[Required]属性标记Code属性。 I could send this validation result back in the response with a 400 "BadRequest" Http status code and validation messages in the response body. 我可以通过响应正文中的400“ BadRequest” Http状态代码和验证消息将验证结果发送回响应中。 But I cannot tell exactly what was wrong and can't distinguish between the wrong XML in example 1, 3 and 4 (no order has been posted, that's the only thing I can see) - for instance. 但是,我无法确切地说出问题所在,也无法在示例1、3和4(无法发布order ,这是我唯一能看到的)中区分出错误的XML。

Requiring that an Order has to be posted with a specific custom XML schema, for example xmlns="http://test.org/OrderSchema.xsd" , I would like to validate if the posted XML is valid with respect to this schema and, if not, send schema validation errors back in the response. 要求必须使用特定的自定义XML模式(例如xmlns="http://test.org/OrderSchema.xsd"发布Order ,我想验证发布的XML相对于该模式是否有效,并且,如果没有,请在响应中发回架构验证错误。 To achieve this I have started with a custom MediaTypeFormatter : 为此,我开始使用自定义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;
    }
}

This works so far as long as everything is correct. 只要一切正确,就可以使用。

But I don't know what to do if msgs.Count > 0 . 但是我不知道msgs.Count > 0怎么办。 How can I "transfer" this validation result list to the Post action or how can I create a Http response that contains those XML schema validation messages? 如何将该验证结果列表“转移”到Post操作,或者如何创建包含那些XML模式验证消息的Http响应?

Also I am unsure if a custom MediaTypeFormatter is the best extensibility point for such a XML schema validation and if my approach isn't the wrong way. 另外,我不确定自定义MediaTypeFormatter是否是此类XML模式验证的最佳扩展点,以及我的方法是否错误。 Would possibly a custom HttpMessageHandler / DelegatingHandler be a better place for this? 定制HttpMessageHandler / DelegatingHandler可能是一个更好的选择吗? Or is there perhaps something much simpler out of the box? 还是开箱即用更简单?

If I were doing this I wouldn't use the Formatter. 如果执行此操作,则不会使用格式化程序。 The primary goal of a formatter is to convert a wire representation to a CLR type. 格式化程序的主要目标是将线路表示形式转换为CLR类型。 Here you have an XML document that you want to validate against a schema which is a different task altogether. 在这里,您有一个XML文档,您要针对完全是另一项任务的模式进行验证。

I would suggest creating a new MessageHandler to do the validation. 我建议创建一个新的MessageHandler进行验证。 Derive from DelegatingHandler and if the content type is application/xml load the content into XDocument and validate. 从DelegatingHandler派生,如果内容类型为application/xml则将内容加载到XDocument中并进行验证。 If it fails, then throw a HttpResponseException. 如果失败,则抛出HttpResponseException。

Just add your MessageHandler to the Configuration.MessageHandlers collection and you are set. 只需将您的MessageHandler添加到Configuration.MessageHandlers集合中,就可以设置好了。

The problem with using a derived XmlMediaTypeFormatter is that you are now executing at some point embedded inside the ObjectContent code and it is likely to be tricky to cleanly exit out. 使用派生的XmlMediaTypeFormatter的问题在于,您现在正在执行嵌入在ObjectContent代码中的某个点,并且干净退出可能很棘手。 Also, making XmlMediaTypeFormatter any more complex is probably not a great idea. 另外,使XmlMediaTypeFormatter变得更加复杂可能也不是一个好主意。

I had a stab at creating the MessageHandler. 我在创建MessageHandler时遇到了麻烦。 I did not actually try running this code, so buyer beware. 我实际上并没有尝试运行此代码,因此请买家当心。 Also, the task stuff gets pretty hairy if you avoid blocking the caller. 此外,如果您避免阻止呼叫者,任务的工作将变得很繁琐。 Maybe someone will clean that code up for me, anyway here it is. 也许有人会为我清理该代码,反正就是这样。

  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);
            }

        }

By trial and error I found a solution (for the WHAT NOW ? placeholder in the question's code): 通过反复试验,我找到了一个解决方案(针对问题代码中的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);
}

...with the response class like this: ...具有这样的响应类:

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

That seems to work and the response looks like this then: 这似乎可行,然后响应如下所示:

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>

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM