Как я могу обнаружить состояние, которое вызывает исключение, прежде чем оно произойдет?
Мне не повезло с этим вопросом, поэтому я подготовил простой тестовый пример, чтобы продемонстрировать проблему.
В приведенном ниже коде можно ли обнаружить, что соединение непригодно для использования, прежде чем пытаться его использовать?
SqlConnection c = new SqlConnection(myConnString);
c.Open(); // creates pool
setAppRole(c); // OK
c.Close(); // returns connection to pool
c = new SqlConnection(myConnString); // gets connection from pool
c.Open(); // ok... but wait for it...
// ??? How to detect KABOOM before it happens?
setAppRole(c); // KABOOM
KABOOM проявляется как ошибка в журнале событий Windows;
Соединение было разорвано, потому что субъект, открывший его, впоследствии принял новый контекст безопасности, а затем попытался сбросить соединение в рамках своего олицетворенного контекста безопасности. Этот сценарий не поддерживается. См. "Обзор олицетворения" в Книгах Онлайн.
... плюс исключение в коде.
setAppRole - это простой метод для установки роли приложения в соединении. Это похоже на это...
static void setAppRole(SqlConnection conn) {
using (IDbCommand cmd = conn.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.CommandText = "exec sp_setapprole ";
cmd.CommandText += string.Format("@rolename='{0}'",myUser);
cmd.CommandText += string.Format(",@password='{0}'",myPassword);
cmd.ExecuteNonQuery();
}
}
В реальном коде делается попытка использовать sp_unsetapprole до закрытия соединения, но это не всегда может быть гарантировано (унаследованное глючное многопоточное приложение). В любом случае все еще кажется разумным ожидать, что он сможет обнаружить kaboom, прежде чем вызвать его.
8 ответов
Короче говоря, это не выглядит так просто.
Моей первой мыслью было запустить этот SQL:
SELECT CASE WHEN USER = 'MyAppRole' THEN 1 ELSE 0 END
Это работает, если вы используете SQL Server Management Studio, но не работает, когда вы запускаете его из кода C#. Проблема в том, что ошибка, которую вы получаете, не возникает при вызове sp_setapprole, а фактически возникает, когда пул соединений вызывает sp_reset_connection. Пул соединений вызывает это, когда вы впервые используете соединение, и до него нет способа войти.
Итак, я думаю, у вас есть четыре варианта:
- Отключите пул соединений, добавив "Pooling=false;" к вашей строке подключения.
- Используйте другой способ подключения к SQL Server. Есть API более низкого уровня, чем ADO.Net, но, честно говоря, это, вероятно, не стоит проблем.
- Как говорит casperOne, вы можете исправить свой код, чтобы правильно закрыть соединение.
- Перехватите исключение и сбросьте пул соединений. Я не уверен, что это будет делать с другими открытыми соединениями, хотя. Пример кода ниже:
class Program
{
static void Main(string[] args)
{
SqlConnection conn = new SqlConnection("Server=(local);Database=Test;UID=Scrap;PWD=password;");
setAppRole(conn);
conn.Close();
setAppRole(conn);
conn.Close();
}
static void setAppRole(SqlConnection conn)
{
for (int i = 0; i < 2; i++)
{
conn.Open();
try
{
using (IDbCommand cmd = conn.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.CommandText = "exec sp_setapprole ";
cmd.CommandText += string.Format("@rolename='{0}'", "MyAppRole");
cmd.CommandText += string.Format(",@password='{0}'", "password1");
cmd.ExecuteNonQuery();
}
}
catch (SqlException ex)
{
if (i == 0 && ex.Number == 0)
{
conn.Close();
SqlConnection.ClearPool(conn);
continue;
}
else
{
throw;
}
}
return;
}
}
}
это фактически происходит, когда пул соединений вызывает sp_reset_connection. Пул соединений вызывает это, когда вы впервые используете соединение, и до него нет способа войти.
Основываясь на ответе Мартина Брауна, вы можете попробовать добавить "Connection Reset=False" в строку подключения как способ "войти до" sp_reset_connection. (См. " Работа с" загрязненными "соединениями" для объяснения многих недостатков этого параметра.)
Ваша проблема - известная проблема с пулом соединений. Предложенный обходной путь - отключить пул соединений... если это настольное приложение, возможно, стоит рассмотреть возможность удержания нескольких открытых соединений вместо этого (также объяснено в статье, приведенной выше).
В наши дни (SQL 2005+) рекомендуется (в разделе " Роли приложений и пулы соединений") "использовать преимущества новых механизмов безопасности, которые вы можете использовать вместо ролей приложений", например EXECUTE AS.
Вы можете проверить c.State (объект ConnectionState), который должен быть одним из следующих:
System.Data.ConnectionState.Broken
System.Data.ConnectionState.Closed
System.Data.ConnectionState.Connecting
System.Data.ConnectionState.Executing
System.Data.ConnectionState.Fetching
System.Data.ConnectionState.Open
@edg: Вы говорите в комментарии: "... только тогда он попадает на реальный сервер и сталкивается с проблемой безопасности, как описано в цитате из msg".
Это указывает на источник вашей проблемы: вы столкнулись с проблемой безопасности, и это кажется неизбежным, потому что вызывающий код принимает другую идентичность, чем использовалась для открытия соединения. Это естественно делает запись в журнале безопасности.
Поскольку изменение идентификатора является заданным, возможно, решение заключается в фильтрации журнала безопасности. В программе просмотра событий есть действие "Фильтровать текущий журнал", которое можно фильтровать по ключевому слову или даже по дате
+ том
Я также опубликовал это в ответ на ваш предыдущий вопрос. При вызове sp_setapprole вы должны вызывать sp_unsetapprole, когда закончите, и решение, которое я предложил, поможет вам:
Обнаружение непригодных пулов SqlConnections
Казалось бы, вы вызываете sp_setapprole, но не вызываете sp_unsetapprole, а затем позволяете соединению просто вернуться в пул.
Я бы предложил использовать структуру (или класс, если вам нужно использовать это в разных методах) с реализацией IDisposable, которая позаботится об этом за вас:
public struct ConnectionManager : IDisposable
{
// The backing for the connection.
private SqlConnection connection;
// The connection.
public SqlConnection Connection { get { return connection; } }
public void Dispose()
{
// If there is no connection, get out.
if (connection == null)
{
// Get out.
return;
}
// Make sure connection is cleaned up.
using (SqlConnection c = connection)
{
// See (1). Create the command for sp_unsetapprole
// and then execute.
using (SqlCommand command = ...)
{
// Execute the command.
command.ExecuteNonQuery();
}
}
}
public ConnectionManager Release()
{
// Create a copy to return.
ConnectionManager retVal = this;
// Set the connection to null.
retVal.connection = null;
// Return the copy.
return retVal;
}
public static ConnectionManager Create()
{
// Create the return value, use a using statement.
using (ConnectionManager cm = new ConnectionManager())
{
// Create the connection and assign here.
// See (2).
cm.connection = ...
// Create the command to call sp_setapprole here.
using (SqlCommand command = ...)
{
// Execute the command.
command.ExecuteNonQuery();
// Return the connection, but call release
// so the connection is still live on return.
return cm.Release();
}
}
}
}
- Вы создадите SqlCommand, который соответствует вызову хранимой процедуры sp_setapprole. Вы можете сгенерировать cookie и сохранить его в закрытой переменной-члене.
- Здесь вы создаете свою связь.
Код клиента тогда выглядит так:
using (ConnectionManager cm = ConnectionManager.Create())
{
// Get the SqlConnection for use.
// No need for a using statement, when Dispose is
// called on the connection manager, the connection will be
// closed.
SqlConnection connection = cm.Connection;
// Use connection appropriately.
}
Разве нет способа очистить пул от всех соединений. SqlPools.Clear или что-то.
Вы можете просто попытаться перехватить исключение и создать новое соединение, что должно заставить пул создать полностью новое соединение.
Я не уверен в вашей проблеме, но я думаю, что вы бы избежали ее, если бы вы создали новые объекты подключения, а не использовали их повторно. Так что вместо того, чтобы делать
c.Open();
blabla;
c.Close();
c.Open();
kaboom...
Вы бы сделали следующее:
using (new SqlConnection ...)
{
c.Open();
blabla;
}
using (new SqlConnection ... )
{
c.Open();
no kaboom?
}
(Пожалуйста, прости псевдокод... Клавиатуру на моем eeepc невозможно использовать...)
Попробуй переехать sp_unsetapprole
(действительно ли это имя спрока? sp_dropapprole
правильный?) setAppRole()
и выполнить его до добавления роли приложения.