3

我试图找到解决方案来验证在POST请求中发送的XML数据是否满足给定的自定义XML模式。使用ASP.NET WebAPI对POST请求进行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 

等等等等

其中I具有所述请求的确认的唯一点是模型绑定:在请求实施例1,3和4 order传入Post方法是null,在示例2中order.Code属性是null我可以通过测试无效或用[Required]属性标记Code属性。我可以将此验证结果发送回响应中,并在响应正文中使用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会是更好的选择吗?或者可能有更简单的开箱即用的东西?

回答

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> 
5

如果我这样做,我不会使用格式化程序。格式化程序的主要目标是将导线表示转换为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); 
      } 

     } 
+0

感谢您的回复,我会尽力。我昨天通过抛出一个'HttpResponseException'找到了类似的解决方案(请参阅我自己的答案)。但我同意一个MediaTypeFormatter似乎不是正确的地方。顺便说一句:你知道如果你有多个MessageHandlers会发生什么?我有另一个基本身份验证,并不希望如果身份验证失败验证处理程序踢任何人。我是否必须确保验证处理程序在MessageHandlers集合中的验证处理程序之前? – Slauma 2012-08-06 10:49:23

+0

我目前没有得到代码工作。在'msgs.Count> 0'情况下,它以Http 500,stacktrace'EndProcessRequest' - >'OnAsyncHandlerCompletion'结尾,'else'情况起作用。我还不太熟悉Task API和WebAPI以找到修复程序。我尝试了一些随机更改,但没有成功。认证消息处理程序必须是第一个要执行的最后一个。但即使身份验证处理程序中的身份验证失败,我仍会输入验证处理程序。也许我必须检查安全主体,然后跳过处理程序,然后,不知何故...... – Slauma 2012-08-06 14:07:13

+0

@Slauma通过添加多个消息处理程序,您可以获得它们的一个链。只需在验证处理程序之后添加验证处理程序。 – 2012-08-06 14:15:53