【问题标题】:How can I detect condition that causes exception before it happens?如何在异常发生之前检测到导致异常的条件?
【发布时间】:2009-02-17 11:58:09
【问题描述】:

我对@9​​87654321@ 不满意,所以我制作了这个尽可能简单的测试用例来演示这个问题。

在下面的代码中,是否可以在尝试使用之前检测到连接不可用?

    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,但不能始终保证(继承有缺陷的多线程应用程序)。无论如何,期望能够在引起爆炸之前检测到爆炸似乎仍然是合理的。

【问题讨论】:

    标签: c# sql-server ado.net connection-pooling application-role


    【解决方案1】:

    简而言之,它看起来不像你可以用任何简单的方式。

    我的第一个想法是运行这个 SQL:

    SELECT CASE WHEN USER = 'MyAppRole' THEN 1 ELSE 0 END
    

    这在您使用 SQL Server Management Studio 时有效,但在您从 C# 代码运行时会失败。问题是在调用 sp_setapprole 时不会发生您遇到的错误,它实际上是在连接池调用 sp_reset_connection 时发生的。当你第一次使用连接并且在它之前没有办法进入时,连接池会调用它。

    所以我猜你有四个选择:

    1. 通过添加“Pooling=false;”关闭连接池到您的连接字符串。
    2. 使用其他方式连接到 SQL Server。有比 ADO.Net 更低级别的 API,但坦率地说,这可能不值得这么麻烦。
    3. 正如 casperOne 所说,您可以修复代码以正确关闭连接。
    4. 捕获异常并重置连接池。不过,我不确定这会对其他打开的连接产生什么影响。示例代码如下:
    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;
            }
        }
    }
    

    【讨论】:

      【解决方案2】:

      它实际上是在连接池调用时发生的 sp_reset_connection。连接池在您第一次使用时调用它 一个连接,在它之前没有办法进入。

      基于Martin Brown's answer,您可以尝试将“Connection Reset=False”添加到连接字符串中,作为“先于”sp_reset_connection 的一种方式。 (有关此设置的许多缺点的说明,请参阅“Working with “soiled” connections”。)

      您的问题是known issue with connection pooling。建议的解决方法是禁用连接池...如果这是一个桌面应用程序,则可能值得考虑打开一些连接(也在the article linked above 中解释)。

      如今 (SQL 2005+) 建议(在 Application Roles and Connection Pooling 下)是“利用您可以使用的 new security mechanisms 来代替应用程序角色”,例如 EXECUTE AS。

      【讨论】:

        【解决方案3】:

        我不确定你的问题,但我认为如果你创建新的连接对象而不是重用它们,你会避免它。所以不要做

        c.Open();
        blabla;
        c.Close();
        c.Open(); 
        kaboom...
        

        您将执行以下操作:

        using (new SqlConnection ...)
        {
          c.Open();
          blabla;
        }
        
        using (new SqlConnection ... )
        {
          c.Open();
          no kaboom?
        }
        

        (请原谅伪代码...我eeepc上的键盘无法使用...)

        【讨论】:

        • 值得考虑,但实际上我每次都会得到一个新的 SqlConnection。连接池不在乎,它返回已关闭的旧连接(这通常很好,并且是预期的)。我删掉了额外的代码来为这个演示获取新的 SqlConnection()。
        • 我已经更新了上面的演示代码以显示 c = new SqlConnection(),我认为显示它并不会太复杂。
        【解决方案4】:

        没有办法清除所有连接池。 SqlPools.Clear 什么的。

        您可以简单地尝试捕获异常并创建一个新连接,这应该会强制池创建一个完整的新连接。

        【讨论】:

        • 是的,我可以清理池子,但是这会破坏首先拥有池子的好处 :-)
        • 在实际代码中,我确实捕获了异常,但这并不能阻止事件日志中的错误消息,这正是我想要实现的。 (在捕获异常后,我可以显式 Close() 和 Open() 从池中获取新连接。这可以正常工作,但会留下事件日志问题。
        【解决方案5】:

        我也发布了这个来回答你之前的问题。当调用 sp_setapprole 时,你应该在完成后调用 sp_unsetapprole,我在那里提出的解决方案会对你有所帮助:

        Detecting unusable pooled 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();
                    }
                }
            }
        }
        
        1. 您将创建与调用 sp_setapprole 存储过程相对应的 SqlCommand。您也可以生成 cookie 并将其存储在私有成员变量中。
        2. 这是您创建连接的地方。

        客户端代码如下所示:

        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.
        }
        

        【讨论】:

        • 谢谢,友好的幽灵,我确实调用了unsetapprole,这个问题是关于它是否可以检测到它没有被调用的场景。
        • @edg:我不相信你能检测到它。我似乎找不到将返回连接上当前角色的 SP,这正是您想要的。上面的结构应该可以在您的应用程序中确定性地删除角色。多线程应该和它无关。
        • @edg:我说多线程与它无关,因为你不是在线程之间共享连接,对吧?如果你是,那是你必须清理的情况。此外,您的代码不能保证在发布时调用 sp_unsetapprole,而我提供的代码可以。
        • 不,不共享线程之间的连接,并且我们有代码可以取消批准,据我们所知,它始终可以正常运行。调试应用程序并不是一个 SO 可以回答的问题,而 检测 可能是失败条件。
        • @edg:我认为你是本末倒置,因为你无法捕捉到异常。您无法检测连接上的应用角色是什么,没有查询/sp 可以做到这一点。
        【解决方案6】:

        您可以检查 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
        

        【讨论】:

        • ConnectionState 在我的情况下总是打开的。打开和关闭没有问题,只是在使用连接时。只有当池连接被重新使用时才会出现问题,它才会到达实际的服务器并遇到消息引用中描述的安全问题
        【解决方案7】:

        @edg:您在评论中说,“...只有这样它才会访问实际服务器并遇到消息引用中描述的安全问题”。

        这指出了您的问题的根源:您正在遇到安全问题,这似乎是不可避免的,因为调用代码假定了另一个身份,而不是用于打开连接。这自然会生成一个安全日志条目。

        由于身份更改是设计使然,因此解决方案可能是过滤安全日志。事件查看器有一个过滤当前日志操作,可以按关键字或 eventid 过滤。

        +汤姆

        【讨论】:

        • 感谢 Tom,如果我可以将这些事件从应用程序日志引导到自定义源(例如 MyAppLog 而不是应用程序),这可能会解决我的问题。不幸的是,这些事件日志消息无法通过 C# 从 .Net 层控制 - 它们属于 SQL Server。
        • (事件记录后过滤有帮助,我需要阻止这些消息或将它们引导到其他地方)
        • ic。嗯,许多服务器产品使用提供程序模型,其中可能存在日志记录提供程序。如果 SQL Server 提供,那么您可以插入一个过滤器作为自定义错误提供程序。快速搜索显示集成服务有这个。但我不知道 SQL Server 本身。
        【解决方案8】:

        尝试将sp_unsetapprole(它真的是存储过程的名称吗?可能sp_dropapprole 是正确的?)移动到setAppRole(),并在添加应用角色之前执行它。

        【讨论】:

        • LOL - sp_dropapprole 会从数据库中完全删除应用程序角色
        • 遗憾的是,在设置应用程序之前无法取消设置它。 :( sp_unsetapprole 需要一个从 sp_setapprole 返回的 cookie(cookie 代码未显示以保持演示简单)
        猜你喜欢
        • 2010-11-14
        • 2014-08-03
        • 2019-01-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-16
        • 2013-03-25
        相关资源
        最近更新 更多