Подтвердить что ты не робот

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

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

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

Edit:

Я создал оболочку для 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; }
    }
}
4b9b3361

Ответ 1

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

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

Конечным предложением было бы использовать SqlTransaction, который затем не зависит от потока; вы можете написать обертку Stream, которая находится вокруг SqlFileStream, и закройте читатель, транзакцию и соединение (и завернутый поток) в Dispose(). WCF вызовет это (через Close()) после обработки результатов.

Ответ 2

Хмм мне может что-то здесь не хватает, но мне кажется, что более простой подход - предоставить поток методу 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);

Ответ 3

Логически ни один из связанных с 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.