如何在HttpModule中执行XSLT转换?

3
我一直在尝试将服务器端的XSLT转换作为IIS HttpModule实现。我的基本方法是在BeginRequest处安装一个新的过滤器,将写入重定向到MemoryStream中,然后在PreSendRequestContent处使用XSLT转换文档并将其写入原始输出流。然而,即使不执行转换,我显然也做错了什么,因为HttpModule似乎只能在第一次页面加载时工作,然后我就无法从服务器获得任何响应,直到重新启动应用程序池。如果进行转换,第一次会得到一个空白页面,然后没有响应。我显然做了些愚蠢的事情,但这是我多年来编写的第一段C#代码(也是我第一次尝试编写HttpModule),我不知道问题可能是什么。我犯了什么错误?(在下面的代码中,我已经注释掉了XSLT部分,并取消注释了一个将缓存内容写入响应的行。)
using System;
using System.IO;
using System.Text;
using System.Web;
using System.Xml;
using System.Xml.Xsl;

namespace Onyx {

    public class OnyxModule : IHttpModule {

        public String ModuleName {
            get { return "OnyxModule"; }
        }

        public void Dispose() {
        }

        public void Init(HttpApplication application) {

            application.BeginRequest += (sender, e) => {
                HttpResponse response = HttpContext.Current.Response;
                response.Filter = new CacheFilter(response.Filter);
                response.Buffer = true;
            };

            application.PreSendRequestContent += (sender, e) => {

                HttpResponse response = HttpContext.Current.Response;
                CacheFilter cache = (CacheFilter)response.Filter;

                response.Filter = cache.originalStream;
                response.Clear();

 /*               XmlReader xml = XmlReader.Create(new StreamReader(cache), new XmlReaderSettings() {
                    ProhibitDtd = false,
                    ConformanceLevel = ConformanceLevel.Auto
                });

                XmlWriter html = XmlWriter.Create(response.OutputStream, new XmlWriterSettings() {
                    ConformanceLevel = ConformanceLevel.Auto
                });

                XslCompiledTransform xslt = new XslCompiledTransform();
                xslt.Load("http://localhost/transformations/test_college.xsl", new XsltSettings() {
                    EnableDocumentFunction = true
                }, new XmlUrlResolver());
                xslt.Transform(xml, html); */

                response.Write(cache.ToString());

                response.Flush();

            };

        }


    }

    public class CacheFilter : MemoryStream {

        public Stream originalStream;
        private MemoryStream cacheStream;

        public CacheFilter(Stream stream) {
            originalStream = stream;
            cacheStream = new MemoryStream();
        }

        public override int Read(byte[] buffer, int offset, int count) {
            return cacheStream.Read(buffer, offset, count);
        }

        public override void Write(byte[] buffer, int offset, int count) {
            cacheStream.Write(buffer, offset, count);
        }

        public override bool CanRead {
            get { return cacheStream.CanRead; }
        }

        public override string ToString() {
            return Encoding.UTF8.GetString(cacheStream.ToArray());
        }

    }

}

1
不要误会,但是你的样本与那种花哨的“Clean Code”相去甚远。 - Filburt
@Jeff:好的,只有两个明显的问题:1)命名-(HttpApplication context)是误导性的。2)将所有东西都塞进Init()中,而根据其名称应该初始化模块。 - Filburt
不会有任何冒犯。我已经更正了参数的命名,但我认为实际上使用Lambda表达式的版本更易于阅读和维护(虽然这可能是因为我不是C#开发人员)。目前我甚至不会称它为原型,但我只想看看是否可以以足够的性能实现我想要做的事情,所以今天我一直在尝试许多迭代来获得一些输出,而不必担心哪个类和方法具有哪些职责。我花了一段时间将处理程序附加到不同的事件... - Rich
...当应用程序很多时,这样做非常繁琐。SomeEvent += (new EventHandler(this.Application_SomeEvent))...在各个地方都有private void Application_SomeEvent(...)。话虽如此,我相信生产版本中每个lambda不会超过两三行。 - Rich
@Filburt: 我认为在这种情况下,一个名为 Application_BeginRequest 的事件处理程序并没有真正需要命名的东西。我倾向于认为定义命名函数的原因有三个:(1)重用功能而不复制它; (2)提高可读性或简化复杂逻辑;(3)抽象出算法的部分以提高将来的可维护性。对于 BeginRequest 处理程序,我不认为这三个条件都适用,但当然我的简单或可读性比 Uncle Bob 更重要... - Rich
显示剩余5条评论
3个回答

3

当你将数据读入MemoryStream后,流的位置会在流的末尾。在将流发送到StreamReader/XmlReader之前,您需要将位置重置为0。

stream.Position = 0;
/* or */
stream.Seek(0, SeekOrigin.Begin);

第二个版本的参数顺序是错误的。我现在已经修改了代码,只能得到一次XSLT输出。第二次加载页面时,我会得到“'',十六进制值0x1F,是无效字符。行1,位置1”,这肯定是一个进步。 - Rich
修正了我的代码..没有智能感知就是这样 ;) 为了进一步调试,您应该将传入的流转储到磁盘上并检查它是否确实是有效的 XML。它可能被压缩了吗? - Mikael Svenson
0x1F 似乎是您的 UTF-8 编码响应的 BOM。您可以指示 UTF8Encoding 省略 BOM:http://msdn.microsoft.com/en-us/library/s064f8w2.aspx - Filburt
1
问题出在静态内容压缩上。当我关闭它时,它就正常工作了(尽管我将XSLT处理移入了过滤器的“写入”方法中)。 - Rich

2
我有点惊讶这个方法居然能正常工作(即使在重置流位置后)。我查看了一下HttpApplication代码,虽然我并不完全理解它,但看起来你可能在请求处理过程中太晚地修改了输出流。
如果你仍然没有解决问题,尝试将第二个处理程序函数附加到PostReleaseRequestState之后的任何一个事件上——要么是UpdateRequestCache,要么是PostUpdateRequestCache。两者都不太适合,但请继续阅读!
由于某种原因,MSDN关于HttpApplication的文档中没有将PreSendRequestContent列入其事件列表,但反编译器显示它的处理程序直到HttpResponse.Flush才被调用。
如果我正确理解代码的话,Response.Flush在调用处理程序之前计算内容长度,因此当它到达这段代码时,它认为响应为空:
if (contentLength > 0L) {
    byte[] bytes = Encoding.ASCII.GetBytes(Convert.ToString(contentLength, 0x10) + "\r\n");
    this._wr.SendResponseFromMemory(bytes, bytes.Length);
    this._httpWriter.Send(this._wr);
    this._wr.SendResponseFromMemory(s_chunkSuffix, s_chunkSuffix.Length);
}

有一些备选路径可能会根据您的入口点和初始条件而被调用,这可能解释了为什么有时候可以工作但有时候不行。但是在进入Flush后,您可能不应该修改响应流。
您正在做一些不太寻常的事情——您并没有以传统意义上的方式过滤响应流(在那里您将一些字节传递给另一个流),因此您可能必须采取一些巧妙的方法来使您当前的设计工作。
另一种选择是使用IHttpHandler来实现它,而不是使用模块——这里有一个很好的例子。它处理从数据库查询中转换输出,但应该很容易适应文件系统数据源。

我尝试将处理程序附加到其他事件,但没有成功。我认为我可能在流方面做错了什么。在这种情况下,HttpHandler 实现不是最佳选择,因为我希望能够透明地转换文件、WCF 数据服务、其他应用程序的输出等。目前,我们使用 URL 重写器将外部 URL 映射到一个 PHP 脚本,该脚本将请求转发到另一个端点,然后使用 libxslt 转换响应,但性能可能更好,并且与透明度相距甚远。 - Rich
Blast - 啊,好吧,我会把这个留在这里,以防其他人也被误导。 - Jeff Sternal

1

即使您不遵循msdn示例,也应该实现HttpApplication.EndRequest

context.EndRequest += (sender, e) => {
    HttpResponse response = HttpContext.Current.Response;
    response.Flush();
};

清理器

// ...

public void Init(HttpApplication application)
{
    // ...
    application.EndRequest += (new EventHandler(this.Application_EndRequest));
}

private void Application_EndRequest(object sender, EventArgs e)
{
    HttpApplication application = (HttpApplication)source;
    HttpContext context = application.Context;
    context.Current.Response.Flush();
}

具有讽刺意味的是,我最初是这样开始的,但后来改用了lambda版本,因为我认为它更简洁。 - Rich

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接