Возврат потока из службы 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; }
}
}
Ответы
Ответ 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.