Как сделать потоки из больших двоичных объектов доступными в старых C # объектах при использовании SqlDataReader? - PullRequest
9 голосов
/ 09 декабря 2010

Это сценарий:

  • Мы храним файлы, например, относительно большие документы (10-300 МБ), в BLOB-объектах в нашей базе данных MSSQL.
  • У нас очень маленький доменмодели, поэтому мы используем чистый подход SqlDataReader для нашего репозитория вместо ORM, чтобы избежать ненужных зависимостей.
  • Мы хотим использовать объекты в контексте сервера на веб-страницах ASP.NET/ASP.NET MVC.
  • Мы не хотим временно хранить большие двоичные объекты в байтах [], чтобы избежать чрезмерного использования памяти на сервере

Итак, я занимался реализацией своего собственного SqlBlobReader.Он наследует Stream и IDisposable, и во время создания экземпляра мы должны предоставить SqlCommand, содержащий запрос, который возвращает одну строку с одним столбцом, который, конечно же, является потоком, который мы хотим передать.Тогда мои объекты домена C # могут иметь свойство типа Stream, которое возвращает реализацию SqlBlobReader.Затем этот поток можно использовать при потоковой передаче в FileContentStream в ASP.net MVC и т. Д.

Он немедленно выполнит ExecuteReader с SequentialAccess, чтобы включить потоковую передачу большого двоичного объекта с сервера MSSQL.Это означает, что мы должны быть осторожны, чтобы утилизировать поток как можно скорее, и что мы всегда лениво создаем экземпляр SqlBlobReader, когда это необходимо, например, используя вызов хранилища внутри наших доменных объектов.

Тогда мой вопрос:

  • Является ли это разумным способом получения потоков больших двоичных объектов на старых доменных объектах при использовании SqlDataReader вместо ORM?
  • Я не эксперт ADO.NET, делает реализациюкажется разумным?

SqlBlobReader.cs:

using System;
using System.Data;
using System.Data.SqlClient;
using System.IO;

namespace Foo
{
   /// <summary>
   /// There must be a SqlConnection that works inside the SqlCommand. Remember to dispose of the object after usage.
   /// </summary>
   public class SqlBlobReader : Stream
   {
      private readonly SqlCommand command;
      private readonly SqlDataReader dataReader;
      private bool disposed = false;
      private long currentPosition = 0;

      /// <summary>
      /// Constructor
      /// </summary>
      /// <param name="command">The supplied <para>sqlCommand</para> must only have one field in select statement, or else the stream won't work. Select just one row, all others will be ignored.</param>
      public SqlBlobReader(SqlCommand command)
      {
         if (command == null)
            throw new ArgumentNullException("command");
         if (command.Connection == null)
            throw new ArgumentException("The internal Connection cannot be null", "command");
         if (command.Connection.State != ConnectionState.Open)
            throw new ArgumentException("The internal Connection must be opened", "command");
         dataReader = command.ExecuteReader(CommandBehavior.SequentialAccess);
         dataReader.Read();
         this.command = command; // only stored for disposal later
      }

      /// <summary>
      /// Not supported
      /// </summary>
      public override long Seek(long offset, SeekOrigin origin)
      {
         throw new NotSupportedException();
      }

      /// <summary>
      /// Not supported
      /// </summary>
      public override void SetLength(long value)
      {
         throw new NotSupportedException();
      }

      public override int Read(byte[] buffer, int index, int count)
      {
         long returned = dataReader.GetBytes(0, currentPosition, buffer, 0, buffer.Length);
         currentPosition += returned;
         return Convert.ToInt32(returned);
      }

      /// <summary>
      /// Not supported
      /// </summary>
      public override void Write(byte[] buffer, int offset, int count)
      {
         throw new NotSupportedException();
      }

      public override bool CanRead
      {
         get { return true; }
      }

      public override bool CanSeek
      {
         get { return false; }
      }

      public override bool CanWrite
      {
         get { return false; }
      }

      public override long Length
      {
         get { throw new NotSupportedException(); }
      }

      public override long Position
      {
         get { throw new NotSupportedException(); }
         set { throw new NotSupportedException(); }
      }

      protected override void Dispose(bool disposing)
      {
         if (!disposed)
         {
            if (disposing)
            {
               if (dataReader != null)
                  dataReader.Dispose();
               SqlConnection conn = null;
               if (command != null)
               {
                  conn = command.Connection;
                  command.Dispose();
               }
               if (conn != null)
                  conn.Dispose();
               disposed = true;
            }
         }
         base.Dispose(disposing);
      }

      public override void Flush()
      {
         throw new NotSupportedException();
      }

   }

}

В Repository.cs:

  public virtual Stream GetDocumentFileStream(int fileId)
  {
     var conn = new SqlConnection {ConnectionString = configuration.ConnectionString};
     var cmd = new SqlCommand
                  {
                     CommandText =
                        "select DocumentFile " +
                        "from MyTable " +
                        "where Id = @Id",
                     Connection = conn,
                  };


     cmd.Parameters.Add("@Id", SqlDbType.Int).Value = fileId;
     conn.Open();
     return new SqlBlobReader(cmd);
  }

В DocumentFile.cs:

  public Stream GetStream()
  {
     return repository.GetDocumentFileStream(Id);
  }

В DocumentController.cs:

  // A download controller in ASP.net MVC 2

  [OutputCache(CacheProfile = "BigFile")]
  public ActionResult Download(int id)
  {
     var document = repository.GetDocument(id);
     return new FileStreamResult(document.DocumentFile.GetStream(), "application/pdf")
               {
                  FileDownloadName = "Foo.pdf";
               };
  }

Ответы [ 3 ]

7 голосов
/ 09 декабря 2010

Есть ошибка;вы игнорируете аргументы пользователя, и вы, вероятно, должны защищать для -ve returned:

  public override int Read(byte[] buffer, int index, int count)
  {
     long returned = dataReader.GetBytes(0, currentPosition,
         buffer, 0, buffer.Length);
     currentPosition += returned;
     return Convert.ToInt32(returned);
  }

должно быть:

  public override int Read(byte[] buffer, int index, int count)
  {
     long returned = dataReader.GetBytes(0, currentPosition,
         buffer, index, count);
     if(returned > 0) currentPosition += returned;
     return (int)returned;
  }

(в противном случае вы пишете не в ту частьбуфера)

Но в целом выглядит хорошо.

1 голос
/ 30 января 2014

Обратите внимание, что .net 4.5 теперь делает это OOB - SqlDataReader.GetStream ()

http://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqldatareader.getstream(v=vs.110).aspx

0 голосов
/ 10 июня 2011

Это великолепно!Спасибо за эту заставку памяти.Помимо исправления Марка, я модифицировал конструктор для открытия соединения и удаления в случае, если при открытии или выполнении не удается уменьшить обработку кода / исключения в вызывающей стороне.(Не знал, что Dispose может быть вызван из конструктора).Конструктор мод:

try
{
    this.command = command;     // store for disposal

    if (command.Connection.State != ConnectionState.Open)
        command.Connection.Open();

    dataReader = command.ExecuteReader(CommandBehavior.SequentialAccess);
    dataReader.Read();            
}
catch (Exception ex)
{
    Dispose();
    throw;
}
...