Возврат потока из службы WCF с использованием SqlFileStream

У меня есть служба WCF, из которой пользователи могут запрашивать большие файлы данных (хранятся в базе данных SQL с включенным FileStream). Эти файлы должны передаваться в потоковом режиме и не загружаться в память перед их отправкой.

Итак, у меня есть следующий метод, который должен возвращать поток, который вызывается службой WCF, чтобы он мог вернуть поток клиенту.

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, говорящий "транзакция прервана". Могу ли я закрыть транзакцию и соединение перед возвратом потока? Любая помощь приветствуется, спасибо

Редактировать:

Я создал обертку для 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; }
    }
}

3 ответа

Решение

Обычно я мог бы предложить обернуть поток в пользовательский поток, который при утилизации закрывает транзакцию, однако IIRC WCF не дает никаких гарантий относительно того, какие потоки что делают, но TransactionScope является специфичным для потока. Таким образом, возможно, лучшим вариантом является копирование данных в MemoryStream (если он не слишком большой) и верните это. Stream.Copy метод в 4.0 должен сделать это бризом, но не забудьте перемотать поток памяти перед финальным return (.Position = 0).

Очевидно, что это будет большой проблемой, если поток большой, но... если поток достаточно большой , чтобы это вызывало беспокойство, то лично я был бы обеспокоен тем фактом, что он работает в TransactionScope вообще, так как это имеет встроенные ограничения по времени и вызывает сериализуемую изоляцию (по умолчанию).

Последнее предложение будет использовать SqlTransaction, который тогда не зависит от потока; Вы могли бы написать Stream Обертка, которая сидит вокруг SqlFileStreamи закройте считыватель, транзакцию и соединение (и завернутый поток) в 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();
}

Похоже, что это работает для меня, и это поддерживает потоковую логику отдельно от кода, связанного с SQL.

Другие вопросы по тегам