1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 using System.Threading;
8 namespace System.Data.SqlClient.ManualTesting.Tests
10 public class SqlNotificationTest : IDisposable
13 private const int CALLBACK_TIMEOUT = 5000; // milliseconds
16 private readonly string _tableName = $"dbo.[SQLDEP_{Guid.NewGuid().ToString()}]";
17 private readonly string _queueName = $"SQLDEP_{Guid.NewGuid().ToString()}";
18 private readonly string _serviceName = $"SQLDEP_{Guid.NewGuid().ToString()}";
19 private readonly string _schemaQueue;
21 // Connection information used by all tests
22 private readonly string _startConnectionString;
23 private readonly string _execConnectionString;
25 public SqlNotificationTest()
27 _startConnectionString = DataTestUtility.TcpConnStr;
28 _execConnectionString = DataTestUtility.TcpConnStr;
30 _schemaQueue = $"[{_queueName}]";
40 #region StartStop_Tests
42 [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
43 public void Test_DoubleStart_SameConnStr()
45 Assert.True(SqlDependency.Start(_startConnectionString), "Failed to start listener.");
47 Assert.False(SqlDependency.Start(_startConnectionString), "Expected failure when trying to start listener.");
49 Assert.False(SqlDependency.Stop(_startConnectionString), "Expected failure when trying to completely stop listener.");
51 Assert.True(SqlDependency.Stop(_startConnectionString), "Failed to stop listener.");
54 [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
55 public void Test_DoubleStart_DifferentConnStr()
57 SqlConnectionStringBuilder cb = new SqlConnectionStringBuilder(_startConnectionString);
59 // just change something that doesn't impact the dependency dispatcher
60 if (cb.ShouldSerialize("connect timeout"))
61 cb.ConnectTimeout = cb.ConnectTimeout + 1;
63 cb.ConnectTimeout = 50;
65 Assert.True(SqlDependency.Start(_startConnectionString), "Failed to start listener.");
69 DataTestUtility.AssertThrowsWrapper<InvalidOperationException>(() => SqlDependency.Start(cb.ToString()));
73 Assert.True(SqlDependency.Stop(_startConnectionString), "Failed to stop listener.");
75 Assert.False(SqlDependency.Stop(cb.ToString()), "Expected failure when trying to completely stop listener.");
79 [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
80 public void Test_Start_DifferentDB()
82 SqlConnectionStringBuilder cb = new SqlConnectionStringBuilder(_startConnectionString)
84 InitialCatalog = "tempdb"
86 string altDatabaseConnectionString = cb.ToString();
88 Assert.True(SqlDependency.Start(_startConnectionString), "Failed to start listener.");
90 Assert.True(SqlDependency.Start(altDatabaseConnectionString), "Failed to start listener.");
92 Assert.True(SqlDependency.Stop(_startConnectionString), "Failed to stop listener.");
94 Assert.True(SqlDependency.Stop(altDatabaseConnectionString), "Failed to stop listener.");
98 #region SqlDependency_Tests
100 [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
101 public void Test_SingleDependency_NoStart()
103 using (SqlConnection conn = new SqlConnection(_execConnectionString))
104 using (SqlCommand cmd = new SqlCommand("SELECT a, b, c FROM " + _tableName, conn))
108 SqlDependency dep = new SqlDependency(cmd);
109 dep.OnChange += delegate (object o, SqlNotificationEventArgs args)
111 Console.WriteLine("4 Notification callback. Type={0}, Info={1}, Source={2}", args.Type, args.Info, args.Source);
114 DataTestUtility.AssertThrowsWrapper<InvalidOperationException>(() => cmd.ExecuteReader());
118 [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
119 public void Test_SingleDependency_Stopped()
121 SqlDependency.Start(_startConnectionString);
122 SqlDependency.Stop(_startConnectionString);
124 using (SqlConnection conn = new SqlConnection(_execConnectionString))
125 using (SqlCommand cmd = new SqlCommand("SELECT a, b, c FROM " + _tableName, conn))
129 SqlDependency dep = new SqlDependency(cmd);
130 dep.OnChange += delegate (object o, SqlNotificationEventArgs args)
132 // Delegate won't be called, since notifications were stoppped
133 Console.WriteLine("5 Notification callback. Type={0}, Info={1}, Source={2}", args.Type, args.Info, args.Source);
136 DataTestUtility.AssertThrowsWrapper<InvalidOperationException>(() => cmd.ExecuteReader());
140 [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
141 public void Test_SingleDependency_AllDefaults_SqlAuth()
143 Assert.True(SqlDependency.Start(_startConnectionString), "Failed to start listener.");
147 // create a new event every time to avoid mixing notification callbacks
148 ManualResetEvent notificationReceived = new ManualResetEvent(false);
149 ManualResetEvent updateCompleted = new ManualResetEvent(false);
151 using (SqlConnection conn = new SqlConnection(_execConnectionString))
152 using (SqlCommand cmd = new SqlCommand("SELECT a, b, c FROM " + _tableName, conn))
156 SqlDependency dep = new SqlDependency(cmd);
157 dep.OnChange += delegate (object o, SqlNotificationEventArgs arg)
159 Assert.True(updateCompleted.WaitOne(CALLBACK_TIMEOUT, false), "Received notification, but update did not complete.");
161 DataTestUtility.AssertEqualsWithDescription(SqlNotificationType.Change, arg.Type, "Unexpected Type value.");
162 DataTestUtility.AssertEqualsWithDescription(SqlNotificationInfo.Update, arg.Info, "Unexpected Info value.");
163 DataTestUtility.AssertEqualsWithDescription(SqlNotificationSource.Data, arg.Source, "Unexpected Source value.");
165 notificationReceived.Set();
171 int count = RunSQL("UPDATE " + _tableName + " SET c=" + Environment.TickCount);
172 DataTestUtility.AssertEqualsWithDescription(1, count, "Unexpected count value.");
174 updateCompleted.Set();
176 Assert.True(notificationReceived.WaitOne(CALLBACK_TIMEOUT, false), "Notification not received within the timeout period");
180 Assert.True(SqlDependency.Stop(_startConnectionString), "Failed to stop listener.");
184 [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
185 public void Test_SingleDependency_CustomQueue_SqlAuth()
187 Assert.True(SqlDependency.Start(_startConnectionString, _queueName), "Failed to start listener.");
191 // create a new event every time to avoid mixing notification callbacks
192 ManualResetEvent notificationReceived = new ManualResetEvent(false);
193 ManualResetEvent updateCompleted = new ManualResetEvent(false);
195 using (SqlConnection conn = new SqlConnection(_execConnectionString))
196 using (SqlCommand cmd = new SqlCommand("SELECT a, b, c FROM " + _tableName, conn))
200 SqlDependency dep = new SqlDependency(cmd, "service=" + _serviceName + ";local database=msdb", 0);
201 dep.OnChange += delegate (object o, SqlNotificationEventArgs args)
203 Assert.True(updateCompleted.WaitOne(CALLBACK_TIMEOUT, false), "Received notification, but update did not complete.");
205 Console.WriteLine("7 Notification callback. Type={0}, Info={1}, Source={2}", args.Type, args.Info, args.Source);
206 notificationReceived.Set();
212 int count = RunSQL("UPDATE " + _tableName + " SET c=" + Environment.TickCount);
213 DataTestUtility.AssertEqualsWithDescription(1, count, "Unexpected count value.");
215 updateCompleted.Set();
217 Assert.False(notificationReceived.WaitOne(CALLBACK_TIMEOUT, false), "Notification should not be received.");
221 Assert.True(SqlDependency.Stop(_startConnectionString, _queueName), "Failed to stop listener.");
226 /// SqlDependecy premature timeout
228 [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
229 public void Test_SingleDependency_Timeout()
231 Assert.True(SqlDependency.Start(_startConnectionString), "Failed to start listener.");
235 // with resolution of 15 seconds, SqlDependency should fire timeout notification only after 45 seconds, leave 5 seconds gap from both sides.
236 const int SqlDependencyTimerResolution = 15; // seconds
237 const int testTimeSeconds = SqlDependencyTimerResolution * 3 - 5;
238 const int minTimeoutEventInterval = testTimeSeconds - 1;
239 const int maxTimeoutEventInterval = testTimeSeconds + SqlDependencyTimerResolution + 1;
241 // create a new event every time to avoid mixing notification callbacks
242 ManualResetEvent notificationReceived = new ManualResetEvent(false);
243 DateTime startUtcTime;
245 using (SqlConnection conn = new SqlConnection(_execConnectionString))
246 using (SqlCommand cmd = new SqlCommand("SELECT a, b, c FROM " + _tableName, conn))
250 // create SqlDependency with timeout
251 SqlDependency dep = new SqlDependency(cmd, null, testTimeSeconds);
252 dep.OnChange += delegate (object o, SqlNotificationEventArgs arg)
254 // notification of Timeout can arrive either from server or from client timer. Handle both situations here:
255 SqlNotificationInfo info = arg.Info;
256 if (info == SqlNotificationInfo.Unknown)
258 // server timed out before the client, replace it with Error to produce consistent output for trun
259 info = SqlNotificationInfo.Error;
262 DataTestUtility.AssertEqualsWithDescription(SqlNotificationType.Change, arg.Type, "Unexpected Type value.");
263 DataTestUtility.AssertEqualsWithDescription(SqlNotificationInfo.Error, arg.Info, "Unexpected Info value.");
264 DataTestUtility.AssertEqualsWithDescription(SqlNotificationSource.Timeout, arg.Source, "Unexpected Source value.");
265 notificationReceived.Set();
269 startUtcTime = DateTime.UtcNow;
273 notificationReceived.WaitOne(TimeSpan.FromSeconds(maxTimeoutEventInterval), false),
274 string.Format("Notification not received within the maximum timeout period of {0} seconds", maxTimeoutEventInterval));
276 // notification received in time, check that it is not too early
277 TimeSpan notificationTime = DateTime.UtcNow - startUtcTime;
279 notificationTime >= TimeSpan.FromSeconds(minTimeoutEventInterval),
281 "Notification was not expected before {0} seconds: received after {1} seconds",
282 minTimeoutEventInterval, notificationTime.TotalSeconds));
286 Assert.True(SqlDependency.Stop(_startConnectionString), "Failed to stop listener.");
292 #region Utility_Methods
293 private static string[] CreateSqlSetupStatements(string tableName, string queueName, string serviceName)
295 return new string[] {
296 string.Format("CREATE TABLE {0}(a INT NOT NULL, b NVARCHAR(10), c INT NOT NULL)", tableName),
297 string.Format("INSERT INTO {0} (a, b, c) VALUES (1, 'foo', 0)", tableName),
298 string.Format("CREATE QUEUE {0}", queueName),
299 string.Format("CREATE SERVICE [{0}] ON QUEUE {1} ([http://schemas.microsoft.com/SQL/Notifications/PostQueryNotification])", serviceName, queueName)
303 private static string[] CreateSqlCleanupStatements(string tableName, string queueName, string serviceName)
305 return new string[] {
306 string.Format("DROP TABLE {0}", tableName),
307 string.Format("DROP SERVICE [{0}]", serviceName),
308 string.Format("DROP QUEUE {0}", queueName)
314 RunSQL(CreateSqlSetupStatements(_tableName, _schemaQueue, _serviceName));
317 private void Cleanup()
319 RunSQL(CreateSqlCleanupStatements(_tableName, _schemaQueue, _serviceName));
322 private int RunSQL(params string[] stmts)
325 using (SqlConnection conn = new SqlConnection(_execConnectionString))
329 SqlCommand cmd = conn.CreateCommand();
331 foreach (string stmt in stmts)
333 cmd.CommandText = stmt;
334 int tmp = cmd.ExecuteNonQuery();
335 count = ((0 <= tmp) ? ((0 <= count) ? count + tmp : tmp) : count);