Как мне реализовать фильтр ответов HTTP для одновременной работы со всем содержимым, без фрагментации

Как упоминалось в нескольких других сообщениях (см. Ссылки ниже), я пытаюсь создать фильтры ответов, чтобы изменить контент, создаваемый другим веб-приложением.

У меня есть базовая логика преобразования строк, работающая и инкапсулированная в фильтры, производные от общего FilterBase. Однако логика должна работать с полным содержимым, а не с его фрагментами. Поэтому мне нужно кэшировать фрагменты по мере их записи и выполнять фильтрацию, когда все записи завершены.

Как показано ниже, я создал новый ResponseFilter, производный от MemoryStream. При записи содержимое кэшируется в другой MemoryStream. На Flush полное содержимое, теперь находящееся в MemoryStream, преобразуется в строку, и срабатывает логика фильтра. Затем измененное содержимое записывается обратно в исходный поток.

Однако при каждом втором запросе (в основном, когда новый фильтр создается поверх предыдущего) выполняется метод Flush предыдущего фильтра. В этот момент приложение падает в методе _outputStream.Write(), поскольку _cachedStream пуст.

Порядок мероприятия следующий:

  1. Первый запрос
  2. Метод записи называется
  3. Метод слива называется
  4. Метод закрытия называется
  5. Метод закрытия называется
  6. В этот момент приложение возвращается и отображается правильный контент.
  7. Второй запрос
  8. Метод слива называется
  9. Сбой приложения на _outputStream.Write. ArgumentOutOfRangeException (смещение).
  10. Продолжить после сбоя (в Visual Studio)
  11. Метод закрытия называется

У меня есть пара вопросов:

  1. Почему Close вызывается дважды?
  2. Почему вызывается Flush после вызова Closed?
  3. По словам Джея ниже, Flush может быть вызван до того, как поток будет полностью прочитан, где должна находиться логика фильтра? В закрытом? Во флеше, но с "при закрытии"?
  4. Какова правильная реализация фильтра ответов, который работает сразу со всем содержимым?

Примечание. Я получаю точно такое же поведение (за исключением событий Close), если не переопределяю метод Close.

public class ResponseFilter : MemoryStream
{
    private readonly Stream _outputStream;
    private MemoryStream _cachedStream = new MemoryStream(1024);

    private readonly FilterBase _filter;

    public ResponseFilter (Stream outputStream, FilterBase filter)
    {
        _outputStream = outputStream;
        _filter = filter;
    }

    // Flush is called on the second, fourth, and so on, page request (second request) with empty content.
    public override void Flush()
    {
        Encoding encoding = HttpContext.Current.Response.ContentEncoding;

        string cachedContent = encoding.GetString(_cachedStream.ToArray());

        // Filter the cached content
        cachedContent = _filter.Filter(cachedContent);

        byte[] buffer = encoding.GetBytes(cachedContent);
        _cachedStream = new MemoryStream();
        _cachedStream.Write(buffer, 0, buffer.Length);

        // Write new content to stream
        _outputStream.Write(_cachedStream.ToArray(), 0, (int)_cachedStream.Length);
        _cachedStream.SetLength(0);

        _outputStream.Flush();
    }

    // Write is called on the first, third, and so on, page request.
    public override void Write(byte[] buffer, int offset, int count)
    {
        // Cache the content.
        _cachedStream.Write(buffer, 0, count);
    }

    public override void Close()
    {
        _outputStream.Close();
    }
}

// Example usage in a custom HTTP Module on the BeginRequest event.
FilterBase transformFilter = new MapServiceJsonResponseFilter();
response.Filter = new ResponseFilter(response.Filter, transformFilter);

Использованная литература:


person Ryan Taylor    schedule 16.12.2010    source источник


Ответы (2)


Благодаря подсказке Джея о том, что Flush вызывается для добавочной записи, я смог заставить фильтр работать по желанию, выполняя логику фильтрации только в том случае, если фильтр закрывается и еще не закрылся. Это гарантирует, что фильтр сбрасывается только один раз, когда поток закрывается. Я сделал это с помощью нескольких простых полей, _isClosing и _isClosed, как показано в итоговом коде ниже.

public class ResponseFilter : MemoryStream
{
    private readonly Stream _outputStream;
    private MemoryStream _cachedStream = new MemoryStream(1024);

    private readonly FilterBase _filter;
    private bool _isClosing;
    private bool _isClosed;

    public ResponseFilter (Stream outputStream, FilterBase filter)
    {
        _outputStream = outputStream;
        _filter = filter;
    }

    public override void Flush()
    {
        if (_isClosing && !_isClosed)
        {
            Encoding encoding = HttpContext.Current.Response.ContentEncoding;

            string cachedContent = encoding.GetString(_cachedStream.ToArray());

            // Filter the cached content
            cachedContent = _filter.Filter(cachedContent);

            byte[] buffer = encoding.GetBytes(cachedContent);
            _cachedStream = new MemoryStream();
            _cachedStream.Write(buffer, 0, buffer.Length);

            // Write new content to stream
            _outputStream.Write(_cachedStream.ToArray(), 0, (int)_cachedStream.Length);
            _cachedStream.SetLength(0);

            _outputStream.Flush();
        }
    }

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

    public override void Close()
    {
        _isClosing = true;

        Flush();

        _isClosed = true;
        _isClosing = false;

        _outputStream.Close();
    }
}

Я еще не нашел ответов на свои другие вопросы выше, поэтому я не буду отмечать этот ответ как исключенный в настоящее время.

person Ryan Taylor    schedule 17.12.2010
comment
Это должен был быть ответ - это помогло мне реализовать фильтр ответов с использованием HtmlAgilityPack для изменения атрибутов на узлах html. Спасибо! - person Dmitriy Khaykin; 06.05.2011
comment
Я не понимаю, почему в переопределении метода Write вы всегда ставите 0 в качестве смещения? - person Durden81; 21.10.2011
comment
+1. И меня интересуют ваши занятия FilterBase и MapServiceJsonResponseFilter... - person jerone; 28.08.2012

Flush не вызывается явно. Возможно, он вызывается, когда код понимает, что нужен новый объект, или, возможно, в результате финализатора.

Я думаю, что после любой инкрементной записи можно вызывать flush, поэтому я не уверен, что вызов flush в любом случае является адекватным признаком полного сообщения.

person jay    schedule 16.12.2010
comment
Даже если он вызывается неявно в результате финализатора, я ожидаю, что _cachedContent будет содержать записанные в него данные. Однако _cachedContent, похоже, не содержит данных. Хорошая мысль об использовании флеша для инкрементной записи. - person Ryan Taylor; 17.12.2010