2010-12-09 65 views
9

Ceci est le scénario:Comment rendre les flux de BLOB disponibles dans les anciens objets C# simples lors de l'utilisation de SqlDataReader?

  • Nous stockons des fichiers, par ex. documents relativement volumineux (10-300MB), en blobs dans notre base de données MSSQL.
  • Nous avons un très petit modèle de domaine, donc nous utilisons l'approche propre SqlDataReader pour notre référentiel, au lieu d'un ORM, pour éviter les dépendances inutiles.
  • Nous souhaitons utiliser les objets dans le contexte du serveur sur les pages Web ASP.NET/ASP.NET MVC.
  • Nous ne voulons pas stocker temporairement les blobs dans l'octet [], afin d'éviter une utilisation élevée de la mémoire sur le serveur

Alors ce que je fais est de mettre en œuvre ma propre SqlBlobReader. Il hérite de Stream et d'IDisposable et lors de l'instanciation, nous devons fournir une SqlCommand contenant une requête qui renvoie une ligne avec une colonne, qui est le blob que nous voulons diffuser, bien sûr. Ensuite, mes objets de domaine C# peuvent avoir une propriété de type Stream qui retourne une implémentation SqlBlobReader. Ce flux peut ensuite être utilisé lors de la diffusion vers un FileContentStream dans ASP.net MVC, etc.

Il exécutera immédiatement un ExecuteReader avec SequentialAccess pour activer la diffusion en continu du blob à partir du serveur MSSQL. Cela signifie que nous devons prendre soin de disposer le flux dès que possible en l'utilisant, et que nous instancions paresseusement SqlBlobReader quand il est nécessaire, par ex. en utilisant un appel de dépôt à l'intérieur de nos objets de domaine.

Ma question est alors:

  • Est-ce une façon intelligente de réaliser des flux de blobs sur des objets de domaine pures et simples lors de l'utilisation SqlDataReader au lieu d'un ORM?
  • Je ne suis pas un expert ADO.NET, la mise en œuvre semble-t-elle raisonnable?

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

    } 

} 

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

En DocumentFile.cs:

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

En 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"; 
       }; 
    } 
+0

Pour information: POCO = Plain Old CLR Object. Non Plain Old C# Object :) –

+0

Ouais, je suis juste très orienté C# jusqu'à ce que je reçoive F # sous ma ceinture. Huh huh. –

Répondre

7

Il y a un bug; vous ignorez les args de l'utilisateur, et vous devriez probablement garder pour -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); 
    } 

devrait probablement:

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

(sinon vous écrivez dans la mauvaise partie de la mémoire tampon)

Mais semble généralement bien.

+0

Merci, je vois ce que vous voulez dire. Je vais réviser. Après mon post, j'ai réalisé que peut-être la mise au rebut de la connexion peut être un peu dangereuse, dans le cas où plusieurs commandes partagent la même connexion. Il pourrait être contrôlé via un argument constructeur, peut-être. Ce n'est pas très probable dans mon scénario, mais ce peut être pour d'autres. –

+0

@Geir - hmm; vous pourriez en faire un drapeau dans le constructeur? –

0

C'est magnifique! Merci pour cet économiseur de mémoire. En plus du correctif de Marc, j'ai modifié le constructeur pour ouvrir la connexion et l'éliminer au cas où l'ouverture ou l'exécution échoue à réduire le traitement du code/des exceptions dans l'appelant. (Je ne savais pas que Dispose pouvait être appelé depuis le constructeur).mod Constructor:

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