使用SqlFileStream从WCF服务返回流

我有一个WCF服务,用户可以从中请求大型数据文件(存储在启用了FileStream的SQL数据库中)。 这些文件应该流式传输,并且在发送之前不会加载到内存中。

所以我有以下方法应该返回一个由WCF服务调用的流,以便它可以将Stream返回给客户端。

public static Stream GetData(string tableName, string columnName, string primaryKeyName, Guid primaryKey) { string sqlQuery = String.Format( "SELECT {0}.PathName(), GET_FILESTREAM_TRANSACTION_CONTEXT() FROM {1} WHERE {2} = @primaryKey", columnName, tableName, primaryKeyName); SqlFileStream stream; using (TransactionScope transactionScope = new TransactionScope()) { byte[] serverTransactionContext; string serverPath; using (SqlConnection sqlConnection = new SqlConnection(ConfigurationManager.ConnectionStrings["ConnString"].ToString())) { sqlConnection.Open(); using (SqlCommand sqlCommand = new SqlCommand(sqlQuery, sqlConnection)) { sqlCommand.Parameters.Add("@primaryKey", SqlDbType.UniqueIdentifier).Value = primaryKey; using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader()) { sqlDataReader.Read(); serverPath = sqlDataReader.GetSqlString(0).Value; serverTransactionContext = sqlDataReader.GetSqlBinary(1).Value; sqlDataReader.Close(); } } } stream = new SqlFileStream(serverPath, serverTransactionContext, FileAccess.Read); transactionScope.Complete(); } return stream; } 

我的问题在于TransactionScope和SqlConnection。 我现在正在做的方式不起作用,我得到一个TransactionAbortedException,说“事务已中止”。 我可以在返回Stream之前关闭事务和连接吗? 感谢任何帮助,谢谢

编辑:

我已经为SqlFileStream创建了一个包装器,它实现了IDisposable,这样我就可以在流处理后关闭所有内容。 似乎工作正常

 public class WcfStream : Stream { private readonly SqlConnection sqlConnection; private readonly SqlDataReader sqlDataReader; private readonly SqlTransaction sqlTransaction; private readonly SqlFileStream sqlFileStream; public WcfStream(string connectionString, string columnName, string tableName, string primaryKeyName, Guid primaryKey) { string sqlQuery = String.Format( "SELECT {0}.PathName(), GET_FILESTREAM_TRANSACTION_CONTEXT() FROM {1} WHERE {2} = @primaryKey", columnName, tableName, primaryKeyName); sqlConnection = new SqlConnection(connectionString); sqlConnection.Open(); sqlTransaction = sqlConnection.BeginTransaction(); using (SqlCommand sqlCommand = new SqlCommand(sqlQuery, sqlConnection, sqlTransaction)) { sqlCommand.Parameters.Add("@primaryKey", SqlDbType.UniqueIdentifier).Value = primaryKey; sqlDataReader = sqlCommand.ExecuteReader(); } sqlDataReader.Read(); string serverPath = sqlDataReader.GetSqlString(0).Value; byte[] serverTransactionContext = sqlDataReader.GetSqlBinary(1).Value; sqlFileStream = new SqlFileStream(serverPath, serverTransactionContext, FileAccess.Read); } protected override void Dispose(bool disposing) { sqlDataReader.Close(); sqlFileStream.Close(); sqlConnection.Close(); } public override void Flush() { sqlFileStream.Flush(); } public override long Seek(long offset, SeekOrigin origin) { return sqlFileStream.Seek(offset, origin); } public override void SetLength(long value) { sqlFileStream.SetLength(value); } public override int Read(byte[] buffer, int offset, int count) { return sqlFileStream.Read(buffer, offset, count); } public override void Write(byte[] buffer, int offset, int count) { sqlFileStream.Write(buffer, offset, count); } public override bool CanRead { get { return sqlFileStream.CanRead; } } public override bool CanSeek { get { return sqlFileStream.CanSeek; } } public override bool CanWrite { get { return sqlFileStream.CanWrite; } } public override long Length { get { return sqlFileStream.Length; } } public override long Position { get { return sqlFileStream.Position; } set { sqlFileStream.Position = value; } } } 

通常我可能会建议将流包装在自定义流中,以便在处理时关闭事务,但IIRC WCF不保证哪些线程执行什么操作,但TransactionScope是特定于线程的。 因此,也许更好的选择是将数据复制到MemoryStream (如果它不是太大)并返回它。 4.0中的Stream.Copy方法应该可以轻而易举,但请记住在最终return.Position = 0 )之前return内存流。

显然,如果流很大,这将是一个大问题……但是,如果流足够大以至于成为一个问题,那么我个人会担心它在TransactionScope 运行的事实,因为它具有内置时间限制,并导致可序列化隔离(默认情况下)。

最后的建议是使用SqlTransaction ,然后它不依赖于线程; 你可以编写一个围绕SqlFileStreamStream包装器,并关闭Dispose()的reader,transaction和connection(以及包装的流Dispose() 。 在处理结果后,WCF将调用它(通过Close() )。

嗯,我可能在这里遗漏了一些东西,但在我看来,更简单的方法是将流提供给WCF方法并从那里写入,而不是尝试返回客户端读取的流?

以下是WCF方法的示例:

 public void WriteFileToStream(FetchFileArgs args, Stream outputStream) { using (SqlConnection conn = CreateOpenConnection()) using (SqlTransaction tran = conn.BeginTransaction(IsolationLevel.ReadCommitted)) using (SqlCommand cmd = conn.CreateCommand()) { cmd.CommandType = CommandType.StoredProcedure; cmd.CommandText = "usp_file"; cmd.Transaction = tran; cmd.Parameters.Add("@FileId", SqlDbType.NVarChar).Value = args.Id; using (SqlDataReader reader = cmd.ExecuteReader()) { if (reader.Read()) { string path = reader.GetString(3); byte[] streamContext = reader.GetSqlBytes(4).Buffer; using (var sqlStream = new SqlFileStream(path, streamContext, FileAccess.Read)) sqlStream.CopyTo(outputStream); } } tran.Commit(); } } 

在我的应用程序中,消费者恰好是一个ASP.NET应用程序,调用代码如下所示:

 _fileStorageProvider.WriteFileToStream(fileId, Response.OutputStream); 

逻辑上,没有SQL相关的东西属于Stream包装类(WcfStream),特别是如果你打算将WcfStream实例发送到外部客户端。

您可以做的是在WcfStream处理或关闭后触发一个事件:

 public class WcfStream : Stream { public Stream SQLStream { get; set; } public event EventHandler StreamClosedEventHandler; protected override void Dispose(bool disposing) { if (disposing) { SQLStream.Dispose(); if (this.StreamClosedEventHandler != null) { this.StreamClosedEventHandler(this, new EventArgs()); } } base.Dispose(disposing); } } 

然后在主代码中,您将事件处理程序连接到StreamClosedEventHandler并关闭所有与sql相关的对象:

 ... WcfStream test = new WcfStream(); test.SQLStream = new SqlFileStream(filePath, txContext, FileAccess.Read); test.StreamClosedEventHandler += new EventHandler((sender, args) => DownloadStreamCompleted(sqlDataReader, sqlConnection)); return test; } private void DownloadStreamCompleted(SqlDataReader sqlDataReader, SQLConnection sqlConnection) { // You might want to commit Transaction here as well sqlDataReader.Close(); sqlConnection.Close(); } 

这看起来对我有用,它使Streaming逻辑与SQL相关的代码分开。