| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223 |
- //------------------------------------------------------------------------------
- // <copyright file="SqlDependency.cs" company="Microsoft">
- // Copyright (c) Microsoft Corporation. All rights reserved.
- // </copyright>
- // <owner current="true" primary="true">[....]</owner>
- // <owner current="true" primary="true">[....]</owner>
- // <owner current="false" primary="false">[....]</owner>
- //------------------------------------------------------------------------------
- namespace System.Data.SqlClient {
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Data;
- using System.Data.Common;
- using System.Data.ProviderBase;
- using System.Data.Sql;
- using System.Data.SqlClient;
- using System.Diagnostics;
- using System.Globalization;
- using System.IO;
- using System.Runtime.CompilerServices;
- using System.Runtime.InteropServices;
- using System.Runtime.Remoting;
- using System.Runtime.Serialization.Formatters.Binary;
- using System.Security;
- using System.Security.Cryptography;
- using System.Security.Permissions;
- using System.Security.Principal;
- using System.Text;
- using System.Threading;
- using System.Net;
- using System.Xml;
- using System.Runtime.Versioning;
- public sealed class SqlDependency {
- // ---------------------------------------------------------------------------------------------------------
- // Private class encapsulating the user/identity information - either SQL Auth username or Windows identity.
- // ---------------------------------------------------------------------------------------------------------
- internal class IdentityUserNamePair {
- private DbConnectionPoolIdentity _identity;
- private string _userName;
- internal IdentityUserNamePair(DbConnectionPoolIdentity identity, string userName) {
- Debug.Assert( (identity == null && userName != null) ||
- (identity != null && userName == null), "Unexpected arguments!");
- _identity = identity;
- _userName = userName;
- }
- internal DbConnectionPoolIdentity Identity {
- get {
- return _identity;
- }
- }
-
- internal string UserName {
- get {
- return _userName;
- }
- }
- override public bool Equals(object value) {
- IdentityUserNamePair temp = (IdentityUserNamePair) value;
- bool result = false;
- if (null == temp) { // If passed value null - false.
- result = false;
- }
- else if (this == temp) { // If instances equal - true.
- result = true;
- }
- else {
- if (_identity != null) {
- if (_identity.Equals(temp._identity)) {
- result = true;
- }
- }
- else if (_userName == temp._userName) {
- result = true;
- }
- }
- return result;
- }
- override public int GetHashCode() {
- int hashValue = 0;
- if (null != _identity) {
- hashValue = _identity.GetHashCode();
- }
- else {
- hashValue = _userName.GetHashCode();
- }
- return hashValue;
- }
- }
- // ----------------------------------------
- // END IdentityHashHelper private class.
- // ----------------------------------------
- // ----------------------------------------------------------------------
- // Private class encapsulating the database, service info and hash logic.
- // ----------------------------------------------------------------------
- private class DatabaseServicePair {
- private string _database;
- private string _service; // Store the value, but don't use for equality or hashcode!
- internal DatabaseServicePair(string database, string service) {
- Debug.Assert(database != null, "Unexpected argument!");
- _database = database;
- _service = service;
- }
- internal string Database {
- get {
- return _database;
- }
- }
- internal string Service {
- get {
- return _service;
- }
- }
- override public bool Equals(object value) {
- DatabaseServicePair temp = (DatabaseServicePair) value;
- bool result = false;
- if (null == temp) { // If passed value null - false.
- result = false;
- }
- else if (this == temp) { // If instances equal - true.
- result = true;
- }
- else if (_database == temp._database) {
- result = true;
- }
- return result;
- }
- override public int GetHashCode() {
- return _database.GetHashCode();
- }
- }
- // ----------------------------------------
- // END IdentityHashHelper private class.
- // ----------------------------------------
- // ----------------------------------------------------------------------------
- // Private class encapsulating the event and it's registered execution context.
- // ----------------------------------------------------------------------------
- internal class EventContextPair {
- private OnChangeEventHandler _eventHandler;
- private ExecutionContext _context;
- private SqlDependency _dependency;
- private SqlNotificationEventArgs _args;
- static private ContextCallback _contextCallback = new ContextCallback(InvokeCallback);
- internal EventContextPair(OnChangeEventHandler eventHandler, SqlDependency dependency) {
- Debug.Assert(eventHandler != null && dependency != null, "Unexpected arguments!");
- _eventHandler = eventHandler;
- _context = ExecutionContext.Capture();
- _dependency = dependency;
- }
- override public bool Equals(object value) {
- EventContextPair temp = (EventContextPair) value;
- bool result = false;
- if (null == temp) { // If passed value null - false.
- result = false;
- }
- else if (this == temp) { // If instances equal - true.
- result = true;
- }
- else {
- if (_eventHandler == temp._eventHandler) { // Handler for same delegates are reference equivalent.
- result = true;
- }
- }
- return result;
- }
- override public int GetHashCode() {
- return _eventHandler.GetHashCode();
- }
- internal void Invoke(SqlNotificationEventArgs args) {
- _args = args;
- ExecutionContext.Run(_context, _contextCallback, this);
- }
- private static void InvokeCallback(object eventContextPair) {
- EventContextPair pair = (EventContextPair) eventContextPair;
- pair._eventHandler(pair._dependency, (SqlNotificationEventArgs) pair._args);
- }
- }
- // ----------------------------------------
- // END EventContextPair private class.
- // ----------------------------------------
- // ----------------
- // Instance members
- // ----------------
- // SqlNotificationRequest required state members
- // Only used for SqlDependency.Id.
- private readonly string _id = Guid.NewGuid().ToString() + ";" + _appDomainKey;
- private string _options; // Concat of service & db, in the form "service=x;local database=y".
- private int _timeout;
- // Various SqlDependency required members
- private bool _dependencyFired = false;
- // SQL BU DT 382314 - we are required to implement our own event collection to preserve ExecutionContext on callback.
- private List<EventContextPair> _eventList = new List<EventContextPair>();
- private object _eventHandlerLock = new object(); // Lock for event serialization.
- // Track the time that this dependency should time out. If the server didn't send a change
- // notification or a time-out before this point then the client will perform a client-side
- // timeout.
- private DateTime _expirationTime = DateTime.MaxValue;
- // Used for invalidation of dependencies based on which servers they rely upon.
- // It's possible we will over invalidate if unexpected server failure occurs (but not server down).
- private List<string> _serverList = new List<string>();
- // --------------
- // Static members
- // --------------
- private static object _startStopLock = new object();
- private static readonly string _appDomainKey = Guid.NewGuid().ToString();
- // Hashtable containing all information to match from a server, user, database triple to the service started for that
- // triple. For each server, there can be N users. For each user, there can be N databases. For each server, user,
- // database, there can only be one service.
- private static Dictionary<string, Dictionary<IdentityUserNamePair, List<DatabaseServicePair>>> _serverUserHash =
- new Dictionary<string, Dictionary<IdentityUserNamePair, List<DatabaseServicePair>>>(StringComparer.OrdinalIgnoreCase);
- private static SqlDependencyProcessDispatcher _processDispatcher = null;
- // The following two strings are used for AppDomain.CreateInstance.
- private static readonly string _assemblyName = (typeof(SqlDependencyProcessDispatcher)).Assembly.FullName;
- private static readonly string _typeName = (typeof(SqlDependencyProcessDispatcher)).FullName;
- // -----------
- // BID members
- // -----------
- internal const Bid.ApiGroup NotificationsTracePoints = (Bid.ApiGroup)0x2000;
- private readonly int _objectID = System.Threading.Interlocked.Increment(ref _objectTypeCount);
- private static int _objectTypeCount; // Bid counter
- internal int ObjectID {
- get {
- return _objectID;
- }
- }
- // ------------
- // Constructors
- // ------------
-
- [System.Security.Permissions.HostProtectionAttribute(ExternalThreading = true)]
- public SqlDependency() : this(null, null, SQL.SqlDependencyTimeoutDefault) {
- }
- [System.Security.Permissions.HostProtectionAttribute(ExternalThreading = true)]
- public SqlDependency(SqlCommand command) : this(command, null, SQL.SqlDependencyTimeoutDefault) {
- }
- [System.Security.Permissions.HostProtectionAttribute(ExternalThreading = true)]
- public SqlDependency(SqlCommand command, string options, int timeout) {
- IntPtr hscp;
- Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependency|DEP> %d#, options: '%ls', timeout: '%d'", ObjectID, options, timeout);
- try {
- if (InOutOfProcHelper.InProc) {
- throw SQL.SqlDepCannotBeCreatedInProc();
- }
- if (timeout < 0) {
- throw SQL.InvalidSqlDependencyTimeout("timeout");
- }
- _timeout = timeout;
- if (null != options) { // Ignore null value - will force to default.
- _options = options;
- }
- AddCommandInternal(command);
- SqlDependencyPerAppDomainDispatcher.SingletonInstance.AddDependencyEntry(this); // Add dep to hashtable with Id.
- }
- finally {
- Bid.ScopeLeave(ref hscp);
- }
- }
- // -----------------
- // Public Properties
- // -----------------
- [
- ResCategoryAttribute(Res.DataCategory_Data),
- ResDescriptionAttribute(Res.SqlDependency_HasChanges)
- ]
- public bool HasChanges {
- get {
- return _dependencyFired;
- }
- }
- [
- ResCategoryAttribute(Res.DataCategory_Data),
- ResDescriptionAttribute(Res.SqlDependency_Id)
- ]
- public string Id {
- get {
- return _id;
- }
- }
- // -------------------
- // Internal Properties
- // -------------------
- internal static string AppDomainKey {
- get {
- return _appDomainKey;
- }
- }
- internal DateTime ExpirationTime {
- get {
- return _expirationTime;
- }
- }
- internal string Options {
- get {
- string result = null;
- if (null != _options) {
- result = _options;
- }
- return result;
- }
- }
- internal static SqlDependencyProcessDispatcher ProcessDispatcher {
- get {
- return _processDispatcher;
- }
- }
- internal int Timeout {
- get {
- return _timeout;
- }
- }
-
- // ------
- // Events
- // ------
- [
- ResCategoryAttribute(Res.DataCategory_Data),
- ResDescriptionAttribute(Res.SqlDependency_OnChange)
- ]
- public event OnChangeEventHandler OnChange {
- // EventHandlers to be fired when dependency is notified.
- add {
- IntPtr hscp;
- Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependency.OnChange-Add|DEP> %d#", ObjectID);
- try {
- if (null != value) {
- SqlNotificationEventArgs sqlNotificationEvent = null;
- lock (_eventHandlerLock) {
- if (_dependencyFired) { // If fired, fire the new event immediately.
- Bid.NotificationsTrace("<sc.SqlDependency.OnChange-Add|DEP> Dependency already fired, firing new event.\n");
- sqlNotificationEvent = new SqlNotificationEventArgs(SqlNotificationType.Subscribe, SqlNotificationInfo.AlreadyChanged, SqlNotificationSource.Client);
- }
- else {
- Bid.NotificationsTrace("<sc.SqlDependency.OnChange-Add|DEP> Dependency has not fired, adding new event.\n");
- EventContextPair pair = new EventContextPair(value, this);
- if (!_eventList.Contains(pair)) {
- _eventList.Add(pair);
- }
- else {
- throw SQL.SqlDependencyEventNoDuplicate(); // SQL BU DT 382314
- }
- }
- }
- if (null != sqlNotificationEvent) { // Delay firing the event until outside of lock.
- value(this, sqlNotificationEvent);
- }
- }
- }
- finally {
- Bid.ScopeLeave(ref hscp);
- }
- }
- remove {
- IntPtr hscp;
- Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependency.OnChange-Remove|DEP> %d#", ObjectID);
- try {
- if (null != value) {
- EventContextPair pair = new EventContextPair(value, this);
- lock (_eventHandlerLock) {
- int index = _eventList.IndexOf(pair);
- if (0 <= index) {
- _eventList.RemoveAt(index);
- }
- }
- }
- }
- finally {
- Bid.ScopeLeave(ref hscp);
- }
- }
- }
- // --------------
- // Public Methods
- // --------------
- [
- ResCategoryAttribute(Res.DataCategory_Data),
- ResDescriptionAttribute(Res.SqlDependency_AddCommandDependency)
- ]
- public void AddCommandDependency(SqlCommand command) {
- // Adds command to dependency collection so we automatically create the SqlNotificationsRequest object
- // and listen for a notification for the added commands.
- IntPtr hscp;
- Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependency.AddCommandDependency|DEP> %d#", ObjectID);
- try {
- if (command == null) {
- throw ADP.ArgumentNull("command");
- }
- AddCommandInternal(command);
- }
- finally {
- Bid.ScopeLeave(ref hscp);
- }
- }
-
- [System.Security.Permissions.ReflectionPermission(System.Security.Permissions.SecurityAction.Assert, MemberAccess=true)]
- private static ObjectHandle CreateProcessDispatcher(_AppDomain masterDomain) {
- return masterDomain.CreateInstance(_assemblyName, _typeName);
- }
-
- // ----------------------------------
- // Static Methods - public & internal
- // ----------------------------------
- // Method to obtain AppDomain reference and then obtain the reference to the process wide dispatcher for
- // Start() and Stop() method calls on the individual SqlDependency instances.
- // SxS: this method retrieves the primary AppDomain stored in native library. Since each System.Data.dll has its own copy of native
- // library, this call is safe in SxS
- [ResourceExposure(ResourceScope.None)]
- [ResourceConsumption(ResourceScope.Process, ResourceScope.Process)]
- private static void ObtainProcessDispatcher() {
- byte[] nativeStorage = SNINativeMethodWrapper.GetData();
- if (nativeStorage == null) {
- Bid.NotificationsTrace("<sc.SqlDependency.ObtainProcessDispatcher|DEP> nativeStorage null, obtaining dispatcher AppDomain and creating ProcessDispatcher.\n");
- #if DEBUG // Possibly expensive, limit to debug.
- Bid.NotificationsTrace("<sc.SqlDependency.ObtainProcessDispatcher|DEP> AppDomain.CurrentDomain.FriendlyName: %ls\n", AppDomain.CurrentDomain.FriendlyName);
- #endif
- _AppDomain masterDomain = SNINativeMethodWrapper.GetDefaultAppDomain();
- if (null != masterDomain) {
- ObjectHandle handle = CreateProcessDispatcher(masterDomain);
- if (null != handle) {
- SqlDependencyProcessDispatcher dependency = (SqlDependencyProcessDispatcher) handle.Unwrap();
- if (null != dependency) {
- _processDispatcher = dependency.SingletonProcessDispatcher; // Set to static instance.
- // Serialize and set in native.
- ObjRef objRef = GetObjRef(_processDispatcher);
- BinaryFormatter formatter = new BinaryFormatter();
- MemoryStream stream = new MemoryStream();
- GetSerializedObject(objRef, formatter, stream);
- SNINativeMethodWrapper.SetData(stream.GetBuffer()); // Native will be forced to synchronize and not overwrite.
- }
- else {
- Bid.NotificationsTrace("<sc.SqlDependency.ObtainProcessDispatcher|DEP|ERR> ERROR - ObjectHandle.Unwrap returned null!\n");
- throw ADP.InternalError(ADP.InternalErrorCode.SqlDependencyObtainProcessDispatcherFailureObjectHandle);
- }
- }
- else {
- Bid.NotificationsTrace("<sc.SqlDependency.ObtainProcessDispatcher|DEP|ERR> ERROR - AppDomain.CreateInstance returned null!\n");
- throw ADP.InternalError(ADP.InternalErrorCode.SqlDependencyProcessDispatcherFailureCreateInstance);
- }
- }
- else {
- Bid.NotificationsTrace("<sc.SqlDependency.ObtainProcessDispatcher|DEP|ERR> ERROR - unable to obtain default AppDomain!\n");
- throw ADP.InternalError(ADP.InternalErrorCode.SqlDependencyProcessDispatcherFailureAppDomain);
- }
- }
- else {
- Bid.NotificationsTrace("<sc.SqlDependency.ObtainProcessDispatcher|DEP> nativeStorage not null, obtaining existing dispatcher AppDomain and ProcessDispatcher.\n");
- #if DEBUG // Possibly expensive, limit to debug.
- Bid.NotificationsTrace("<sc.SqlDependency.ObtainProcessDispatcher|DEP> AppDomain.CurrentDomain.FriendlyName: %ls\n", AppDomain.CurrentDomain.FriendlyName);
- #endif
- BinaryFormatter formatter = new BinaryFormatter();
- MemoryStream stream = new MemoryStream(nativeStorage);
- _processDispatcher = GetDeserializedObject(formatter, stream); // Deserialize and set for appdomain.
- Bid.NotificationsTrace("<sc.SqlDependency.ObtainProcessDispatcher|DEP> processDispatcher obtained, ID: %d\n", _processDispatcher.ObjectID);
- }
- }
- // ---------------------------------------------------------
- // Static security asserted methods - limit scope of assert.
- // ---------------------------------------------------------
- [SecurityPermission(SecurityAction.Assert, Flags=SecurityPermissionFlag.RemotingConfiguration)]
- private static ObjRef GetObjRef(SqlDependencyProcessDispatcher _processDispatcher) {
- return RemotingServices.Marshal(_processDispatcher);
- }
- [SecurityPermission(SecurityAction.Assert, Flags=SecurityPermissionFlag.SerializationFormatter)]
- private static void GetSerializedObject(ObjRef objRef, BinaryFormatter formatter, MemoryStream stream) {
- formatter.Serialize(stream, objRef);
- }
- [SecurityPermission(SecurityAction.Assert, Flags=SecurityPermissionFlag.SerializationFormatter)]
- private static SqlDependencyProcessDispatcher GetDeserializedObject(BinaryFormatter formatter, MemoryStream stream) {
- object result = formatter.Deserialize(stream);
- Debug.Assert(result.GetType() == typeof(SqlDependencyProcessDispatcher), "Unexpected type stored in native!");
- return (SqlDependencyProcessDispatcher) result;
- }
-
- // -------------------------
- // Static Start/Stop methods
- // -------------------------
- [System.Security.Permissions.HostProtectionAttribute(ExternalThreading = true)]
- public static bool Start(string connectionString) {
- return Start(connectionString, null, true);
- }
- [System.Security.Permissions.HostProtectionAttribute(ExternalThreading = true)]
- public static bool Start(string connectionString, string queue) {
- return Start(connectionString, queue, false);
- }
- internal static bool Start(string connectionString, string queue, bool useDefaults) {
- IntPtr hscp;
- Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependency.Start|DEP> AppDomainKey: '%ls', queue: '%ls'", AppDomainKey, queue);
- try {
- // The following code exists in Stop as well. It exists here to demand permissions as high in the stack
- // as possible.
- if (InOutOfProcHelper.InProc) {
- throw SQL.SqlDepCannotBeCreatedInProc();
- }
- if (ADP.IsEmpty(connectionString)) {
- if (null == connectionString) {
- throw ADP.ArgumentNull("connectionString");
- }
- else {
- throw ADP.Argument("connectionString");
- }
- }
- if (!useDefaults && ADP.IsEmpty(queue)) { // If specified but null or empty, use defaults.
- useDefaults = true;
- queue = null; // Force to null - for proper hashtable comparison for default case.
- }
- // Create new connection options for demand on their connection string. We modify the connection string
- // and assert on our modified string when we create the container.
- SqlConnectionString connectionStringObject = new SqlConnectionString(connectionString);
- connectionStringObject.DemandPermission();
- if (connectionStringObject.LocalDBInstance!=null) {
- LocalDBAPI.DemandLocalDBPermissions();
- }
- // End duplicate Start/Stop logic.
- bool errorOccurred = false;
- bool result = false;
- lock (_startStopLock) {
- try {
- if (null == _processDispatcher) { // Ensure _processDispatcher reference is present - inside lock.
- ObtainProcessDispatcher();
- }
- if (useDefaults) { // Default listener.
- string server = null;
- DbConnectionPoolIdentity identity = null;
- string user = null;
- string database = null;
- string service = null;
- bool appDomainStart = false;
- RuntimeHelpers.PrepareConstrainedRegions();
- try { // CER to ensure that if Start succeeds we add to hash completing setup.
- // Start using process wide default service/queue & database from connection string.
- result = _processDispatcher.StartWithDefault( connectionString,
- out server,
- out identity,
- out user,
- out database,
- ref service,
- _appDomainKey,
- SqlDependencyPerAppDomainDispatcher.SingletonInstance,
- out errorOccurred,
- out appDomainStart);
- Bid.NotificationsTrace("<sc.SqlDependency.Start|DEP> Start (defaults) returned: '%d', with service: '%ls', server: '%ls', database: '%ls'\n", result, service, server, database);
- }
- finally {
- if (appDomainStart && !errorOccurred) { // If success, add to hashtable.
- IdentityUserNamePair identityUser = new IdentityUserNamePair(identity, user);
- DatabaseServicePair databaseService = new DatabaseServicePair(database, service);
- if (!AddToServerUserHash(server, identityUser, databaseService)) {
- try {
- Stop(connectionString, queue, useDefaults, true);
- }
- catch (Exception e) { // Discard stop failure!
- if (!ADP.IsCatchableExceptionType(e)) {
- throw;
- }
- ADP.TraceExceptionWithoutRethrow(e); // Discard failure, but trace for now.
- Bid.NotificationsTrace("<sc.SqlDependency.Start|DEP|ERR> Exception occurred from Stop() after duplicate was found on Start().\n");
- }
- throw SQL.SqlDependencyDuplicateStart();
- }
- }
- }
- }
- else { // Start with specified service/queue & database.
- result = _processDispatcher.Start(connectionString,
- queue,
- _appDomainKey,
- SqlDependencyPerAppDomainDispatcher.SingletonInstance);
- Bid.NotificationsTrace("<sc.SqlDependency.Start|DEP> Start (user provided queue) returned: '%d'\n", result);
- // No need to call AddToServerDatabaseHash since if not using default queue user is required
- // to provide options themselves.
- }
- }
- catch (Exception e) {
- if (!ADP.IsCatchableExceptionType(e)) {
- throw;
- }
- ADP.TraceExceptionWithoutRethrow(e); // Discard failure, but trace for now.
- Bid.NotificationsTrace("<sc.SqlDependency.Start|DEP|ERR> Exception occurred from _processDispatcher.Start(...), calling Invalidate(...).\n");
- throw;
- }
- }
- return result;
- }
- finally {
- Bid.ScopeLeave(ref hscp);
- }
- }
- [System.Security.Permissions.HostProtectionAttribute(ExternalThreading = true)]
- public static bool Stop(string connectionString) {
- return Stop(connectionString, null, true, false);
- }
-
- [System.Security.Permissions.HostProtectionAttribute(ExternalThreading = true)]
- public static bool Stop(string connectionString, string queue) {
- return Stop(connectionString, queue, false, false);
- }
-
- internal static bool Stop(string connectionString, string queue, bool useDefaults, bool startFailed) {
- IntPtr hscp;
- Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependency.Stop|DEP> AppDomainKey: '%ls', queue: '%ls'", AppDomainKey, queue);
- try {
- // The following code exists in Stop as well. It exists here to demand permissions as high in the stack
- // as possible.
- if (InOutOfProcHelper.InProc) {
- throw SQL.SqlDepCannotBeCreatedInProc();
- }
- if (ADP.IsEmpty(connectionString)) {
- if (null == connectionString) {
- throw ADP.ArgumentNull("connectionString");
- }
- else {
- throw ADP.Argument("connectionString");
- }
- }
- if (!useDefaults && ADP.IsEmpty(queue)) { // If specified but null or empty, use defaults.
- useDefaults = true;
- queue = null; // Force to null - for proper hashtable comparison for default case.
- }
- // Create new connection options for demand on their connection string. We modify the connection string
- // and assert on our modified string when we create the container.
- SqlConnectionString connectionStringObject = new SqlConnectionString(connectionString);
- connectionStringObject.DemandPermission();
- if (connectionStringObject.LocalDBInstance!=null) {
- LocalDBAPI.DemandLocalDBPermissions();
- }
- // End duplicate Start/Stop logic.
- bool result = false;
- lock (_startStopLock) {
- if (null != _processDispatcher) { // If _processDispatcher null, no Start has been called.
- try {
- string server = null;
- DbConnectionPoolIdentity identity = null;
- string user = null;
- string database = null;
- string service = null;
- if (useDefaults) {
- bool appDomainStop = false;
- RuntimeHelpers.PrepareConstrainedRegions();
- try { // CER to ensure that if Stop succeeds we remove from hash completing teardown.
- // Start using process wide default service/queue & database from connection string.
- result = _processDispatcher.Stop( connectionString,
- out server,
- out identity,
- out user,
- out database,
- ref service,
- _appDomainKey,
- out appDomainStop);
- }
- finally {
- if (appDomainStop && !startFailed) { // If success, remove from hashtable.
- Debug.Assert(!ADP.IsEmpty(server) && !ADP.IsEmpty(database), "Server or Database null/Empty upon successfull Stop()!");
- IdentityUserNamePair identityUser = new IdentityUserNamePair(identity, user);
- DatabaseServicePair databaseService = new DatabaseServicePair(database, service);
- RemoveFromServerUserHash(server, identityUser, databaseService);
- }
- }
- }
- else {
- bool ignored = false;
- result = _processDispatcher.Stop( connectionString,
- out server,
- out identity,
- out user,
- out database,
- ref queue,
- _appDomainKey,
- out ignored);
- // No need to call RemoveFromServerDatabaseHash since if not using default queue user is required
- // to provide options themselves.
- }
- }
- catch (Exception e) {
- if (!ADP.IsCatchableExceptionType(e)) {
- throw;
- }
- ADP.TraceExceptionWithoutRethrow(e); // Discard failure, but trace for now.
- }
- }
- }
- return result;
- }
- finally {
- Bid.ScopeLeave(ref hscp);
- }
- }
- // --------------------------------
- // General static utility functions
- // --------------------------------
- private static bool AddToServerUserHash(string server, IdentityUserNamePair identityUser, DatabaseServicePair databaseService) {
- IntPtr hscp;
- Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependency.AddToServerUserHash|DEP> server: '%ls', database: '%ls', service: '%ls'", server, databaseService.Database, databaseService.Service);
- try {
- bool result = false;
- lock (_serverUserHash) {
- Dictionary<IdentityUserNamePair, List<DatabaseServicePair>> identityDatabaseHash;
- if (!_serverUserHash.ContainsKey(server)) {
- Bid.NotificationsTrace("<sc.SqlDependency.AddToServerUserHash|DEP> Hash did not contain server, adding.\n");
- identityDatabaseHash = new Dictionary<IdentityUserNamePair, List<DatabaseServicePair>>();
- _serverUserHash.Add(server, identityDatabaseHash);
- }
- else {
- identityDatabaseHash = _serverUserHash[server];
- }
- List<DatabaseServicePair> databaseServiceList;
- if (!identityDatabaseHash.ContainsKey(identityUser)) {
- Bid.NotificationsTrace("<sc.SqlDependency.AddToServerUserHash|DEP> Hash contained server but not user, adding user.\n");
- databaseServiceList = new List<DatabaseServicePair>();
- identityDatabaseHash.Add(identityUser, databaseServiceList);
- }
- else {
- databaseServiceList = identityDatabaseHash[identityUser];
- }
- if (!databaseServiceList.Contains(databaseService)) {
- Bid.NotificationsTrace("<sc.SqlDependency.AddToServerUserHash|DEP> Adding database.\n");
- databaseServiceList.Add(databaseService);
- result = true;
- }
- else {
- Bid.NotificationsTrace("<sc.SqlDependency.AddToServerUserHash|DEP|ERR> ERROR - hash already contained server, user, and database - we will throw!.\n");
- }
- }
-
- return result;
- }
- finally {
- Bid.ScopeLeave(ref hscp);
- }
- }
- private static void RemoveFromServerUserHash(string server, IdentityUserNamePair identityUser, DatabaseServicePair databaseService) {
- IntPtr hscp;
- Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependency.RemoveFromServerUserHash|DEP> server: '%ls', database: '%ls', service: '%ls'", server, databaseService.Database, databaseService.Service);
- try {
- lock (_serverUserHash) {
- Dictionary<IdentityUserNamePair, List<DatabaseServicePair>> identityDatabaseHash;
- if (_serverUserHash.ContainsKey(server)) {
- identityDatabaseHash = _serverUserHash[server];
-
- List<DatabaseServicePair> databaseServiceList;
- if (identityDatabaseHash.ContainsKey(identityUser)) {
- databaseServiceList = identityDatabaseHash[identityUser];
- int index = databaseServiceList.IndexOf(databaseService);
- if (index >= 0) {
- Bid.NotificationsTrace("<sc.SqlDependency.RemoveFromServerUserHash|DEP> Hash contained server, user, and database - removing database.\n");
- databaseServiceList.RemoveAt(index);
- if (databaseServiceList.Count == 0) {
- Bid.NotificationsTrace("<sc.SqlDependency.RemoveFromServerUserHash|DEP> databaseServiceList count 0, removing the list for this server and user.\n");
- identityDatabaseHash.Remove(identityUser);
- if (identityDatabaseHash.Count == 0) {
- Bid.NotificationsTrace("<sc.SqlDependency.RemoveFromServerUserHash|DEP> identityDatabaseHash count 0, removing the hash for this server.\n");
- _serverUserHash.Remove(server);
- }
- }
- }
- else {
- Bid.NotificationsTrace("<sc.SqlDependency.RemoveFromServerUserHash|DEP|ERR> ERROR - hash contained server and user but not database!\n");
- Debug.Assert(false, "Unexpected state - hash did not contain database!");
- }
- }
- else {
- Bid.NotificationsTrace("<sc.SqlDependency.RemoveFromServerUserHash|DEP|ERR> ERROR - hash contained server but not user!\n");
- Debug.Assert(false, "Unexpected state - hash did not contain user!");
- }
- }
- else {
- Bid.NotificationsTrace("<sc.SqlDependency.RemoveFromServerUserHash|DEP|ERR> ERROR - hash did not contain server!\n");
- Debug.Assert(false, "Unexpected state - hash did not contain server!");
- }
- }
- }
- finally {
- Bid.ScopeLeave(ref hscp);
- }
- }
- internal static string GetDefaultComposedOptions(string server, string failoverServer, IdentityUserNamePair identityUser, string database) {
- // Server must be an exact match, but user and database only needs to match exactly if there is more than one
- // for the given user or database passed. That is ambiguious and we must fail.
- IntPtr hscp;
- Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependency.GetDefaultComposedOptions|DEP> server: '%ls', failoverServer: '%ls', database: '%ls'", server, failoverServer, database);
- try {
- string result;
- lock (_serverUserHash) {
- if (!_serverUserHash.ContainsKey(server)) {
- if (0 == _serverUserHash.Count) { // Special error for no calls to start.
- Bid.NotificationsTrace("<sc.SqlDependency.GetDefaultComposedOptions|DEP|ERR> ERROR - no start calls have been made, about to throw.\n");
- throw SQL.SqlDepDefaultOptionsButNoStart();
- }
- else if (!ADP.IsEmpty(failoverServer) && _serverUserHash.ContainsKey(failoverServer)) {
- Bid.NotificationsTrace("<sc.SqlDependency.GetDefaultComposedOptions|DEP> using failover server instead\n");
- server = failoverServer;
- }
- else {
- Bid.NotificationsTrace("<sc.SqlDependency.GetDefaultComposedOptions|DEP|ERR> ERROR - not listening to this server, about to throw.\n");
- throw SQL.SqlDependencyNoMatchingServerStart();
- }
- }
- Dictionary<IdentityUserNamePair, List<DatabaseServicePair>> identityDatabaseHash = _serverUserHash[server];
- List<DatabaseServicePair> databaseList = null;
- if (!identityDatabaseHash.ContainsKey(identityUser)) {
- if (identityDatabaseHash.Count > 1) {
- Bid.NotificationsTrace("<sc.SqlDependency.GetDefaultComposedOptions|DEP|ERR> ERROR - not listening for this user, but listening to more than one other user, about to throw.\n");
- throw SQL.SqlDependencyNoMatchingServerStart();
- }
- else {
- // Since only one user, - use that.
- // Foreach - but only one value present.
- foreach (KeyValuePair<IdentityUserNamePair, List<DatabaseServicePair>> entry in identityDatabaseHash) {
- databaseList = entry.Value;
- break; // Only iterate once.
- }
- }
- }
- else {
- databaseList = identityDatabaseHash[identityUser];
- }
- DatabaseServicePair pair = new DatabaseServicePair(database, null);
- DatabaseServicePair resultingPair = null;
- int index = databaseList.IndexOf(pair);
- if (index != -1) { // Exact match found, use it.
- resultingPair = databaseList[index];
- }
- if (null != resultingPair) { // Exact database match.
- database = FixupServiceOrDatabaseName(resultingPair.Database); // Fixup in place.
- string quotedService = FixupServiceOrDatabaseName(resultingPair.Service);
- result = "Service="+quotedService+";Local Database="+database;
- }
- else { // No exact database match found.
- if (databaseList.Count == 1) { // If only one database for this server/user, use it.
- object[] temp = databaseList.ToArray(); // Must copy, no other choice but foreach.
- resultingPair = (DatabaseServicePair) temp[0];
- Debug.Assert(temp.Length == 1, "If databaseList.Count==1, why does copied array have length other than 1?");
- string quotedDatabase = FixupServiceOrDatabaseName(resultingPair.Database);
- string quotedService = FixupServiceOrDatabaseName(resultingPair.Service);
- result = "Service="+quotedService+";Local Database="+quotedDatabase;
- }
- else { // More than one database for given server, ambiguous - fail the default case!
- Bid.NotificationsTrace("<sc.SqlDependency.GetDefaultComposedOptions|DEP|ERR> ERROR - SqlDependency.Start called multiple times for this server/user, but no matching database.\n");
- throw SQL.SqlDependencyNoMatchingServerDatabaseStart();
- }
- }
- }
- Debug.Assert(!ADP.IsEmpty(result), "GetDefaultComposedOptions should never return null or empty string!");
- Bid.NotificationsTrace("<sc.SqlDependency.GetDefaultComposedOptions|DEP> resulting options: '%ls'.\n", result);
- return result;
- }
- finally {
- Bid.ScopeLeave(ref hscp);
- }
- }
- // ----------------
- // Internal Methods
- // ----------------
- // Called by SqlCommand upon execution of a SqlNotificationRequest class created by this dependency. We
- // use this list for a reverse lookup based on server.
- internal void AddToServerList(string server) {
- IntPtr hscp;
- Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependency.AddToServerList|DEP> %d#, server: '%ls'", ObjectID, server);
- try {
- lock (_serverList) {
- int index = _serverList.BinarySearch(server, StringComparer.OrdinalIgnoreCase);
- if (0 > index) { // If less than 0, item was not found in list.
- Bid.NotificationsTrace("<sc.SqlDependency.AddToServerList|DEP> Server not present in hashtable, adding server: '%ls'.\n", server);
- index = ~index; // BinarySearch returns the 2's compliment of where the item should be inserted to preserver a sorted list after insertion.
- _serverList.Insert(index, server);
- }
- }
- }
- finally {
- Bid.ScopeLeave(ref hscp);
- }
- }
- internal bool ContainsServer(string server) {
- lock (_serverList) {
- return _serverList.Contains(server);
- }
- }
- internal string ComputeHashAndAddToDispatcher(SqlCommand command) {
- IntPtr hscp;
- Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependency.ComputeHashAndAddToDispatcher|DEP> %d#, SqlCommand: %d#", ObjectID, command.ObjectID);
- try {
- // Create a string representing the concatenation of the connection string, command text and .ToString on all parameter values.
- // This string will then be mapped to unique notification ID (new GUID). We add the guid and the hash to the app domain
- // dispatcher to be able to map back to the dependency that needs to be fired for a notification of this
- // command.
- // VSTS 59821: add Connection string to prevent redundant notifications when same command is running against different databases or SQL servers
- //
- string commandHash = ComputeCommandHash(command.Connection.ConnectionString, command); // calculate the string representation of command
- string idString = SqlDependencyPerAppDomainDispatcher.SingletonInstance.AddCommandEntry(commandHash, this); // Add to map.
- Bid.NotificationsTrace("<sc.SqlDependency.ComputeHashAndAddToDispatcher|DEP> computed id string: '%ls'.\n", idString);
- return idString;
- }
- finally {
- Bid.ScopeLeave(ref hscp);
- }
- }
- internal void Invalidate(SqlNotificationType type, SqlNotificationInfo info, SqlNotificationSource source) {
- IntPtr hscp;
- Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependency.Invalidate|DEP> %d#", ObjectID);
- try {
- List<EventContextPair> eventList = null;
- lock (_eventHandlerLock) {
- if (_dependencyFired &&
- SqlNotificationInfo.AlreadyChanged != info &&
- SqlNotificationSource.Client != source) {
- if (ExpirationTime < DateTime.UtcNow) {
- // There is a small window in which SqlDependencyPerAppDomainDispatcher.TimeoutTimerCallback
- // raises Timeout event but before removing this event from the list. If notification is received from
- // server in this case, we will hit this code path.
- // It is safe to ignore this race condition because no event is sent to user and no leak happens.
- Bid.NotificationsTrace("<sc.SqlDependency.Invalidate|DEP> ignore notification received after timeout!");
- }
- else {
- Debug.Assert(false, "Received notification twice - we should never enter this state!");
- Bid.NotificationsTrace("<sc.SqlDependency.Invalidate|DEP|ERR> ERROR - notification received twice - we should never enter this state!");
- }
- }
- else {
- // It is the invalidators responsibility to remove this dependency from the app domain static hash.
- _dependencyFired = true;
- eventList = _eventList;
- _eventList = new List<EventContextPair>(); // Since we are firing the events, null so we do not fire again.
- }
- }
- if (eventList != null) {
- Bid.NotificationsTrace("<sc.SqlDependency.Invalidate|DEP> Firing events.\n");
- foreach(EventContextPair pair in eventList) {
- pair.Invoke(new SqlNotificationEventArgs(type, info, source));
- }
- }
- }
- finally {
- Bid.ScopeLeave(ref hscp);
- }
- }
- // This method is used by SqlCommand.
- internal void StartTimer(SqlNotificationRequest notificationRequest) {
- IntPtr hscp;
- Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependency.StartTimer|DEP> %d#", ObjectID);
- try {
- if(_expirationTime == DateTime.MaxValue) {
- Bid.NotificationsTrace("<sc.SqlDependency.StartTimer|DEP> We've timed out, executing logic.\n");
- int seconds = SQL.SqlDependencyServerTimeout;
- if (0 != _timeout) {
- seconds = _timeout;
- }
- if (notificationRequest != null && notificationRequest.Timeout < seconds && notificationRequest.Timeout != 0) {
- seconds = notificationRequest.Timeout;
- }
- // VSDD 563926: we use UTC to check if SqlDependency is expired, need to use it here as well.
- _expirationTime = DateTime.UtcNow.AddSeconds(seconds);
- SqlDependencyPerAppDomainDispatcher.SingletonInstance.StartTimer(this);
- }
- }
- finally {
- Bid.ScopeLeave(ref hscp);
- }
- }
-
- // ---------------
- // Private Methods
- // ---------------
- private void AddCommandInternal(SqlCommand cmd) {
- if (cmd != null) { // Don't bother with BID if command null.
- IntPtr hscp;
- Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependency.AddCommandInternal|DEP> %d#, SqlCommand: %d#", ObjectID, cmd.ObjectID);
- try {
- SqlConnection connection = cmd.Connection;
- if (cmd.Notification != null) {
- // Fail if cmd has notification that is not already associated with this dependency.
- if (cmd._sqlDep == null || cmd._sqlDep != this) {
- Bid.NotificationsTrace("<sc.SqlDependency.AddCommandInternal|DEP|ERR> ERROR - throwing command has existing SqlNotificationRequest exception.\n");
- throw SQL.SqlCommandHasExistingSqlNotificationRequest();
- }
- }
- else {
- bool needToInvalidate = false;
- lock (_eventHandlerLock) {
- if (!_dependencyFired) {
- cmd.Notification = new SqlNotificationRequest();
- cmd.Notification.Timeout = _timeout;
-
- // Add the command - A dependancy should always map to a set of commands which haven't fired.
- if (null != _options) { // Assign options if user provided.
- cmd.Notification.Options = _options;
- }
- cmd._sqlDep = this;
- }
- else {
- // We should never be able to enter this state, since if we've fired our event list is cleared
- // and the event method will immediately fire if a new event is added. So, we should never have
- // an event to fire in the event list once we've fired.
- Debug.Assert(0 == _eventList.Count, "How can we have an event at this point?");
- if (0 == _eventList.Count) { // Keep logic just in case.
- Bid.NotificationsTrace("<sc.SqlDependency.AddCommandInternal|DEP|ERR> ERROR - firing events, though it is unexpected we have events at this point.\n");
- needToInvalidate = true; // Delay invalidation until outside of lock.
- }
- }
- }
- if (needToInvalidate) {
- Invalidate(SqlNotificationType.Subscribe, SqlNotificationInfo.AlreadyChanged, SqlNotificationSource.Client);
- }
- }
- }
- finally {
- Bid.ScopeLeave(ref hscp);
- }
- }
- }
- private string ComputeCommandHash(string connectionString, SqlCommand command) {
- IntPtr hscp;
- Bid.NotificationsScopeEnter(out hscp, "<sc.SqlDependency.ComputeCommandHash|DEP> %d#, SqlCommand: %d#", ObjectID, command.ObjectID);
- try {
- // Create a string representing the concatenation of the connection string, the command text and .ToString on all its parameter values.
- // This string will then be mapped to the notification ID.
- // All types should properly support a .ToString for the values except
- // byte[], char[], and XmlReader.
- // NOTE - I hope this doesn't come back to bite us. :(
- StringBuilder builder = new StringBuilder();
- // add the Connection string and the Command text
- builder.AppendFormat("{0};{1}", connectionString, command.CommandText);
- // append params
- for (int i=0; i<command.Parameters.Count; i++) {
- object value = command.Parameters[i].Value;
- if (value == null || value == DBNull.Value) {
- builder.Append("; NULL");
- }
- else {
- Type type = value.GetType();
- if (type == typeof(Byte[])) {
- builder.Append(";");
- byte[] temp = (byte[]) value;
- for (int j=0; j<temp.Length; j++) {
- builder.Append(temp[j].ToString("x2", CultureInfo.InvariantCulture));
- }
- }
- else if (type == typeof(Char[])) {
- builder.Append((char[]) value);
- }
- else if (type == typeof(XmlReader)) {
- builder.Append(";");
- // Cannot .ToString XmlReader - just allocate GUID.
- // This means if XmlReader is used, we will not reuse IDs.
- builder.Append(Guid.NewGuid().ToString());
- }
- else {
- builder.Append(";");
- builder.Append(value.ToString());
- }
- }
- }
- string result = builder.ToString();
- Bid.NotificationsTrace("<sc.SqlDependency.ComputeCommandHash|DEP> ComputeCommandHash result: '%ls'.\n", result);
- return result;
- }
- finally {
- Bid.ScopeLeave(ref hscp);
- }
- }
- // Basic copy of function in SqlConnection.cs for ChangeDatabase and similar functionality. Since this will
- // only be used for default service and database provided by server, we do not need to worry about an already
- // quoted value.
- static internal string FixupServiceOrDatabaseName(string name) {
- if (!ADP.IsEmpty(name)) {
- return "\"" + name.Replace("\"", "\"\"") + "\"";
- }
- else {
- return name;
- }
- }
- }
- }
|