Возврат потока из службы WCF с использованием SqlFileStream - PullRequest
14 голосов
/ 19 сентября 2011

У меня есть служба 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 ]

8 голосов
/ 19 сентября 2011

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

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

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

2 голосов
/ 05 июня 2013

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

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

...