2016-08-03 10:59:49 +00:00
//------------------------------------------------------------------------------
// <copyright file="SqlDependency.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
2017-08-21 15:34:15 +00:00
// <owner current="true" primary="true">Microsoft</owner>
// <owner current="true" primary="true">Microsoft</owner>
// <owner current="false" primary="false">Microsoft</owner>
2016-08-03 10:59:49 +00:00
//------------------------------------------------------------------------------
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 ;
}
}
}
}