使用 SqlDataReader 时,如何使 BLOB 流在普通旧 C# 对象中可用?

发布于 2024-10-07 09:57:57 字数 5417 浏览 0 评论 0原文

场景如下:

  • 我们将文件(例如相对较大的文档 (10-300MB))存储在 MSSQL 数据库中的 blob 中。
  • 我们有一个非常小的域模型,因此我们对存储库使用干净的 SqlDataReader 方法,而不是 ORM,以避免不必要的依赖关系。
  • 我们想要在 ASP.NET/ASP.NET MVC 网页上的服务器上下文中使用对象。
  • 我们不想将 blob 临时存储在 byte[] 中,以避免服务器上的内存使用率过高

,所以我一直在做的是实现我自己的 SqlBlobReader。它继承了 Stream 和 IDisposable,在实例化期间,我们必须提供一个 SqlCommand,其中包含一个返回一行一列的查询,当然,这就是我们想要流式传输的 blob。然后,我的 C# 域对象可以具有 Stream 类型的属性,该属性返回 SqlBlobReader 实现。然后,当流式传输到 ASP.net MVC 等中的 FileContentStream 时,可以使用此流。

它将立即使用 SequentialAccess 执行 ExecuteReader,以启用来自 MSSQL 服务器的 Blob 流式传输。这意味着我们在使用流时必须小心地尽快处理它,并且我们总是在需要时延迟实例化 SqlBlobReader,例如在域对象内使用存储库调用。

我的问题是:

  • 当使用 SqlDataReader 而不是 ORM 时,这是在普通旧域对象上实现 blob 流的明智方法吗?
  • 我不是 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";
               };
  }

This is the scenario:

  • We store files, e.g. relatively large documents (10-300MB), in blobs in our MSSQL database.
  • We have a very small domain model so we use the clean SqlDataReader approach for our repository, instead of an ORM, to avoid unnecessary dependencies.
  • We want to use the objects in server context on ASP.NET/ASP.NET MVC web pages.
  • We do not want to temporarily store the blobs in byte[], to avoid high memory usage on the server

So what I have been doing is to implement my own SqlBlobReader. It inherits Stream and IDisposable and during instantiation we must supply a SqlCommand containing a query that returns one row with one column, which is the blob we want to stream, of course. Then my C# domain objects can have a property of type Stream which returns a SqlBlobReader implementation. This stream can then be used when streaming to a FileContentStream in ASP.net MVC, etc.

It will immediately do an ExecuteReader with SequentialAccess to enable streaming of the blob from the MSSQL server. This means that we must be careful to dispose the stream ASAP when using it, and that we always lazily instantiate SqlBlobReader when it is needed, e.g. using a repository call inside our domain objects.

My question is then:

  • Is this a smart way of achieving streams of blobs on plain old domain objects when using SqlDataReader instead of an ORM?
  • I'm not a ADO.NET expert, does the implementation seem reasonable?

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

   }

}

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

In DocumentFile.cs:

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

In 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";
               };
  }

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

静谧幽蓝 2024-10-14 09:57:57

有一个错误;您忽略了用户的参数,并且您可能应该保护 -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;
  }

否则您将写入缓冲区的错误部分)

但通常看起来不错。

There's a bug; you are ignoring the user's args, and you should probably guard for -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);
  }

should probably be:

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

(otherwise you are writing into the wrong part of the buffer)

But generally looks good.

北风几吹夏 2024-10-14 09:57:57

太漂亮了!感谢这个内存保护程序。除了 Marc 的修复之外,我还修改了构造函数以打开连接并进行处置,以防打开或执行失败以减少调用者中的代码/异常处理。 (不知道可以从构造函数调用 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;
}

That's gorgeous! Thanks for this memory saver. Besides Marc's fix I modified the constructor to open connection and dispose in case the open or execute fails to reduce code/exception handling in caller. (Didn't know Dispose could be called from constructor). Constructor mod:

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;
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文