使用 SqlFileStream 从 WCF 服务返回 Stream

发布于 2024-12-05 07:42:38 字数 4494 浏览 2 评论 0原文

我有一个 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; }
    }
}

I have a WCF service, from which users can request large datafiles (stored in an SQL database with FileStream enabled). These files should be streamed, and not loaded into memory before sending them off.

So I have the following method that should return a stream, which is called by the WCF service, so that it can return the Stream to the client.

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

My problem is with the TransactionScope and the SqlConnection. The way I'm doing it right now doesn't work, I get a TransactionAbortedException saying "The transaction has aborted". Can I close the transaction and the connection before returning the Stream? Any help is appreciated, thank you

Edit:

I've created a wrapper for a SqlFileStream, that implements IDisposable so that I can close everything up once the stream is disposed. Seems to be working fine

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

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

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

发布评论

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

评论(3

凉薄对峙 2024-12-12 07:42:38

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

显然,如果流很大,这将是一个大问题,...但是,如果流足够大,成为一个问题,那么个人我会请注意它完全在 TransactionScope 中运行,因为它具有内置时间限制,并会导致可序列化隔离(默认情况下)。

最后的建议是使用 SqlTransaction,它不依赖于线程;您可以编写一个位于 SqlFileStream 周围的 Stream 包装器,并在 Dispose() 中关闭读取器、事务和连接(以及包装的流)代码>. WCF 将在处理结果后调用它(通过 Close())。

Normally I might suggest wrapping the stream in a custom stream that closes the transaction when disposed, however IIRC WCF makes no guarantees about which threads do what, but TransactionScope is thread-specific. As such, perhaps the better option is to copy the data into a MemoryStream (if it isn't too big) and return that. The Stream.Copy method in 4.0 should make that a breeze, but remember to rewind the memory-stream before the final return (.Position = 0).

Obviously this will be a big problem if the stream is big, ... but, if the stream is big enough for that to be a concern, then personally I'd be concerned at the fact that it is running in TransactionScope at all, since that has inbuilt time limits, and causes serializable isolation (by default).

A final suggestion would be to use a SqlTransaction, which is then not thread-dependent; you could write a Stream wrapper that sits around the SqlFileStream, and close the reader, transaction and connection (and the wrapped stream) in the Dispose(). WCF will call that (via Close()) after processing the results.

慕巷 2024-12-12 07:42:38

嗯,我可能在这里遗漏了一些东西,但在我看来,一种更简单的方法是将流提供给 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);

Hmm I might be missing something here, but it seems to me a simpler approach would be to provide the stream to the WCF method and writing to it from there, rather than trying to return a stream which the client reads from?

Here's an example for a WCF method:

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

In my app, the consumer happens to be an ASP.NET application, and the calling code looks like this:

_fileStorageProvider.WriteFileToStream(fileId, Response.OutputStream);
琴流音 2024-12-12 07:42:38

从逻辑上讲,与 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();
}

:为我工作,它使流逻辑与 SQL 相关代码分开。

Logically none of the SQL related stuff belongs to Stream wrapper class (WcfStream) especially if you intend to send WcfStream instance to external clients.

What you could’ve done was to have an event that would be triggered once WcfStream is disposed or closed:

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

Then in you main code you would hook up an event handler to StreamClosedEventHandler and close all sql-related objects there as such:

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

This looks to be working for me and it keeps Streaming logic separate from SQL-related code.

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