mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-25 17:24:17 -05:00
3278 Kusto Unit Test Refactor (#1053)
* 3278 Moved functions related to Metadata from DataSourceFactory to MetadataFactory.cs * 3278 Refactored DataSourceFactory to not be static. Added IDataSourceFactory interface. * 3278 Refactored IKustoIntellisenseHelper.cs to not be static. Added IKustoIntellisenseHelper.cs interface. * 3278 Removed unused functions from Scripter and deleted ScripterCore.cs because it was unused. Refactored Scripter.cs methods to not be static and created IScripter.cs * 3278 Refactored datsasourceConnectionFactory in ConnectionService to use MEF through the InitializeService function * 3278 Refactored IAutoCompleteHelper to not be static and added IAutoCompleteHelper interface. * 3278 Removed unused classes DatabaseFullAccessException and FeatureWithFullDbAccess. Refactored ObjectExplorerService to use ImportingConstructor attribute. Removed commented out in KustoResultsReader. Removed unused functions from DatabaseLocksManager. Removed unused IDataSourceConnectionFactory from ConnectionService * 3278 Moved SqlConnectionOpener class to new file. Added new interfaces IConnectedBindingQueue.cs and ISqlConnectionOpener.cs Added sqlConnectionOpener to dependency in ConnectedBindingQueue. * 3278 Removed needsMetadata param from ConnectedBindingQueue. Added param to AddConnectionContext where it's used * 3278 Refactored ConnectedBindingQueue to use MEF. Refactored usages to run against interface. * 3278 Corrected ServiceHost namespace. Removed unused dependency to sqlToolsContext in LanguageService. Updated dependency in MetadataService and LanguageService to IProtocolEndpoint instead of ServiceHost * 3278 Added back NextResult function and summary to KustoResultsReader. Renamed instance to _instance in ConnectionService and DatabaseLocksManager to stay consistent. Changed OpenDataSourceConnection to private in ConnectionService * 3278 Converted helper methods back to static and removed backing interfaces * 3278 Reverted AdminService > InitializeService to use ServiceHost as param
This commit is contained in:
12
src/Microsoft.Kusto.ServiceLayer/Scripting/IScripter.cs
Normal file
12
src/Microsoft.Kusto.ServiceLayer/Scripting/IScripter.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Microsoft.Kusto.ServiceLayer.DataSource;
|
||||
using Microsoft.Kusto.ServiceLayer.Scripting.Contracts;
|
||||
using Microsoft.SqlServer.Management.Sdk.Sfc;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.Scripting
|
||||
{
|
||||
public interface IScripter
|
||||
{
|
||||
string SelectFromTableOrView(IDataSource dataSource, Urn urn);
|
||||
string AlterFunction(IDataSource dataSource, ScriptingObject scriptingObject);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ namespace Microsoft.Kusto.ServiceLayer.Scripting
|
||||
/// </summary>
|
||||
public class ScriptAsScriptingOperation : SmoScriptingOperation
|
||||
{
|
||||
private readonly IScripter _scripter;
|
||||
private static readonly Dictionary<string, SqlServerVersion> scriptCompatibilityMap = LoadScriptCompatibilityMap();
|
||||
/// <summary>
|
||||
/// Left delimiter for an named object
|
||||
@@ -37,15 +38,11 @@ namespace Microsoft.Kusto.ServiceLayer.Scripting
|
||||
/// </summary>
|
||||
public const char RightDelimiter = ']';
|
||||
|
||||
public ScriptAsScriptingOperation(ScriptingParams parameters, IDataSource dataSource): base(parameters)
|
||||
public ScriptAsScriptingOperation(ScriptingParams parameters, string azureAccountToken, IScripter scripter) : base(parameters)
|
||||
{
|
||||
Validate.IsNotNull("dataSource", dataSource);
|
||||
DataSource = dataSource;
|
||||
}
|
||||
|
||||
public ScriptAsScriptingOperation(ScriptingParams parameters, string azureAccountToken) : base(parameters)
|
||||
{
|
||||
DataSource = DataSourceFactory.Create(DataSourceType.Kusto, this.Parameters.ConnectionString, azureAccountToken);
|
||||
DataSource = DataSourceFactory.Create(DataSourceType.Kusto, this.Parameters.ConnectionString,
|
||||
azureAccountToken);
|
||||
_scripter = scripter;
|
||||
}
|
||||
|
||||
internal IDataSource DataSource { get; set; }
|
||||
@@ -150,7 +147,7 @@ namespace Microsoft.Kusto.ServiceLayer.Scripting
|
||||
// select from table
|
||||
if (string.Equals(scriptingObject.Type, "Table", StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
return new Scripter().SelectFromTableOrView(dataSource, objectUrn);
|
||||
return _scripter.SelectFromTableOrView(dataSource, objectUrn);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
@@ -163,7 +160,7 @@ namespace Microsoft.Kusto.ServiceLayer.Scripting
|
||||
|
||||
if (string.Equals(scriptingObject.Type, "Function", StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
return new Scripter().AlterFunction(dataSource, scriptingObject);
|
||||
return _scripter.AlterFunction(dataSource, scriptingObject);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
@@ -177,7 +174,7 @@ namespace Microsoft.Kusto.ServiceLayer.Scripting
|
||||
/// <param name="objectName">The object name.</param>
|
||||
/// <param name="schemaQualify">Whether to schema qualify the object or not</param>
|
||||
/// <returns>The object name, quoted as appropriate and schema-qualified if the option is set</returns>
|
||||
static private string GenerateSchemaQualifiedName(string schema, string objectName, bool schemaQualify)
|
||||
private static string GenerateSchemaQualifiedName(string schema, string objectName, bool schemaQualify)
|
||||
{
|
||||
var qualifiedName = new StringBuilder();
|
||||
|
||||
|
||||
@@ -3,73 +3,34 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlServer.Management.Smo;
|
||||
using Microsoft.SqlServer.Management.SqlParser.Intellisense;
|
||||
using Microsoft.SqlServer.Management.Common;
|
||||
using System.Composition;
|
||||
using Microsoft.Kusto.ServiceLayer.Scripting.Contracts;
|
||||
using Microsoft.Kusto.ServiceLayer.DataSource;
|
||||
using Microsoft.SqlServer.Management.Sdk.Sfc;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.Scripting
|
||||
{
|
||||
internal partial class Scripter
|
||||
[Export(typeof(IScripter))]
|
||||
public class Scripter : IScripter
|
||||
{
|
||||
private void Initialize()
|
||||
public string SelectFromTableOrView(IDataSource dataSource, Urn urn)
|
||||
{
|
||||
// Instantiate the mapping dictionaries
|
||||
StringBuilder selectQuery = new StringBuilder();
|
||||
|
||||
// Mapping for supported type
|
||||
AddSupportedType(DeclarationType.Table, "Table", "table", typeof(Table));
|
||||
AddSupportedType(DeclarationType.View, "View", "view", typeof(View));
|
||||
AddSupportedType(DeclarationType.StoredProcedure, "StoredProcedure", "stored procedure", typeof(StoredProcedure));
|
||||
AddSupportedType(DeclarationType.Schema, "Schema", "schema", typeof(Schema));
|
||||
AddSupportedType(DeclarationType.UserDefinedDataType, "UserDefinedDataType", "user-defined data type", typeof(UserDefinedDataType));
|
||||
AddSupportedType(DeclarationType.UserDefinedTableType, "UserDefinedTableType", "user-defined table type", typeof(UserDefinedTableType));
|
||||
AddSupportedType(DeclarationType.Synonym, "Synonym", "", typeof(Synonym));
|
||||
AddSupportedType(DeclarationType.ScalarValuedFunction, "Function", "scalar-valued function", typeof(UserDefinedFunction));
|
||||
AddSupportedType(DeclarationType.TableValuedFunction, "Function", "table-valued function", typeof(UserDefinedFunction));
|
||||
// TODOKusto: Can we combine this with snippets. All queries generated here could also be snippets.
|
||||
// TODOKusto: Extract into the Kusto folder.
|
||||
selectQuery.Append($"{KustoQueryUtils.EscapeName(urn.GetAttribute("Name"))}");
|
||||
selectQuery.Append($"{KustoQueryUtils.StatementSeparator}");
|
||||
selectQuery.Append("limit 1000");
|
||||
|
||||
// Mapping for database engine edition
|
||||
targetDatabaseEngineEditionMap.Add(DatabaseEngineEdition.Unknown, "SqlServerEnterpriseEdition"); //default case
|
||||
targetDatabaseEngineEditionMap.Add(DatabaseEngineEdition.Personal, "SqlServerPersonalEdition");
|
||||
targetDatabaseEngineEditionMap.Add(DatabaseEngineEdition.Standard, "SqlServerStandardEdition");
|
||||
targetDatabaseEngineEditionMap.Add(DatabaseEngineEdition.Enterprise, "SqlServerEnterpriseEdition");
|
||||
targetDatabaseEngineEditionMap.Add(DatabaseEngineEdition.Express, "SqlServerExpressEdition");
|
||||
targetDatabaseEngineEditionMap.Add(DatabaseEngineEdition.SqlDatabase, "SqlAzureDatabaseEdition");
|
||||
targetDatabaseEngineEditionMap.Add(DatabaseEngineEdition.SqlDataWarehouse, "SqlDatawarehouseEdition");
|
||||
targetDatabaseEngineEditionMap.Add(DatabaseEngineEdition.SqlStretchDatabase, "SqlServerStretchEdition");
|
||||
targetDatabaseEngineEditionMap.Add(DatabaseEngineEdition.SqlManagedInstance, "SqlServerManagedInstance");
|
||||
return selectQuery.ToString();
|
||||
}
|
||||
|
||||
// Mapping for database engine type
|
||||
serverVersionMap.Add(9, "Script90Compat");
|
||||
serverVersionMap.Add(10, "Script100Compat");
|
||||
serverVersionMap.Add(11, "Script110Compat");
|
||||
serverVersionMap.Add(12, "Script120Compat");
|
||||
serverVersionMap.Add(13, "Script140Compat");
|
||||
serverVersionMap.Add(14, "Script140Compat");
|
||||
serverVersionMap.Add(15, "Script140Compat");
|
||||
|
||||
// Mapping the object types for scripting
|
||||
objectScriptMap.Add("table", "Table");
|
||||
objectScriptMap.Add("view", "View");
|
||||
objectScriptMap.Add("function", "Function");
|
||||
objectScriptMap.Add("storedprocedure", "Procedure");
|
||||
objectScriptMap.Add("userdefinedfunction", "Function");
|
||||
objectScriptMap.Add("tablevaluedfunction", "Function");
|
||||
objectScriptMap.Add("userdefineddatatype", "Type");
|
||||
objectScriptMap.Add("user", "User");
|
||||
objectScriptMap.Add("default", "Default");
|
||||
objectScriptMap.Add("rule", "Rule");
|
||||
objectScriptMap.Add("databaserole", "Role");
|
||||
objectScriptMap.Add("applicationrole", "Application Role");
|
||||
objectScriptMap.Add("sqlassembly", "Assembly");
|
||||
objectScriptMap.Add("ddltrigger", "Trigger");
|
||||
objectScriptMap.Add("synonym", "Synonym");
|
||||
objectScriptMap.Add("xmlschemacollection", "Xml Schema Collection");
|
||||
objectScriptMap.Add("schema", "Schema");
|
||||
objectScriptMap.Add("planguide", "sp_create_plan_guide");
|
||||
objectScriptMap.Add("userdefinedtype", "Type");
|
||||
objectScriptMap.Add("userdefinedaggregate", "Aggregate");
|
||||
objectScriptMap.Add("fulltextcatalog", "Fulltext Catalog");
|
||||
objectScriptMap.Add("userdefinedtabletype", "Type");
|
||||
public string AlterFunction(IDataSource dataSource, ScriptingObject scriptingObject)
|
||||
{
|
||||
var functionName = scriptingObject.Name.Substring(0, scriptingObject.Name.IndexOf('('));
|
||||
return dataSource.GenerateAlterFunctionScript(functionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,683 +0,0 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.SqlServer.Management.Common;
|
||||
using Microsoft.SqlServer.Management.Smo;
|
||||
using Microsoft.SqlServer.Management.SqlParser.Intellisense;
|
||||
using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
|
||||
using Microsoft.SqlServer.Management.SqlParser.Parser;
|
||||
using Microsoft.SqlTools.Hosting.Protocol;
|
||||
using Microsoft.Kusto.ServiceLayer.Connection;
|
||||
using Microsoft.Kusto.ServiceLayer.LanguageServices;
|
||||
using Microsoft.Kusto.ServiceLayer.Utility;
|
||||
using Microsoft.Kusto.ServiceLayer.Workspace.Contracts;
|
||||
using Microsoft.Kusto.ServiceLayer.Scripting.Contracts;
|
||||
using Microsoft.Kusto.ServiceLayer.DataSource;
|
||||
using Location = Microsoft.Kusto.ServiceLayer.Workspace.Contracts.Location;
|
||||
using Microsoft.SqlServer.Management.Sdk.Sfc;
|
||||
using System.Text;
|
||||
using System.Data;
|
||||
using Range = Microsoft.Kusto.ServiceLayer.Workspace.Contracts.Range;
|
||||
|
||||
namespace Microsoft.Kusto.ServiceLayer.Scripting
|
||||
{
|
||||
internal partial class Scripter
|
||||
{
|
||||
private bool error;
|
||||
private string errorMessage;
|
||||
private IDataSource DataSource { get; set; }
|
||||
private ConnectionInfo connectionInfo;
|
||||
private string tempPath;
|
||||
|
||||
// Dictionary that holds the object name (as appears on the TSQL create statement)
|
||||
private Dictionary<DeclarationType, string> sqlObjectTypes = new Dictionary<DeclarationType, string>();
|
||||
|
||||
private Dictionary<string, string> sqlObjectTypesFromQuickInfo = new Dictionary<string, string>();
|
||||
|
||||
private Dictionary<DatabaseEngineEdition, string> targetDatabaseEngineEditionMap = new Dictionary<DatabaseEngineEdition, string>();
|
||||
|
||||
private Dictionary<int, string> serverVersionMap = new Dictionary<int, string>();
|
||||
|
||||
private Dictionary<string, string> objectScriptMap = new Dictionary<string, string>();
|
||||
|
||||
internal Scripter() {}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a Peek Definition helper object
|
||||
/// </summary>
|
||||
/// <param name="dataSource">Data Source</param>
|
||||
internal Scripter(IDataSource dataSource, ConnectionInfo connInfo)
|
||||
{
|
||||
this.DataSource = dataSource;
|
||||
this.connectionInfo = connInfo;
|
||||
this.tempPath = FileUtilities.GetPeekDefinitionTempFolder();
|
||||
Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the given type, scriptgetter and the typeName string to the respective dictionaries
|
||||
/// </summary>
|
||||
private void AddSupportedType(DeclarationType type, string typeName, string quickInfoType, Type smoObjectType)
|
||||
{
|
||||
sqlObjectTypes.Add(type, typeName);
|
||||
if (!string.IsNullOrEmpty(quickInfoType))
|
||||
{
|
||||
sqlObjectTypesFromQuickInfo.Add(quickInfoType.ToLowerInvariant(), typeName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the script of the selected token based on the type of the token
|
||||
/// </summary>
|
||||
/// <param name="declarationItems"></param>
|
||||
/// <param name="tokenText"></param>
|
||||
/// <param name="schemaName"></param>
|
||||
/// <returns>Location object of the script file</returns>
|
||||
internal DefinitionResult GetScript(ParseResult parseResult, Position position, IMetadataDisplayInfoProvider metadataDisplayInfoProvider, string tokenText, string schemaName)
|
||||
{
|
||||
int parserLine = position.Line;
|
||||
int parserColumn = position.Character;
|
||||
// Get DeclarationItems from The Intellisense Resolver for the selected token. The type of the selected token is extracted from the declarationItem.
|
||||
IEnumerable<Declaration> declarationItems = GetCompletionsForToken(parseResult, parserLine, parserColumn, metadataDisplayInfoProvider);
|
||||
if (declarationItems != null && declarationItems.Count() > 0)
|
||||
{
|
||||
foreach (Declaration declarationItem in declarationItems)
|
||||
{
|
||||
if (declarationItem.Title == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
StringComparison caseSensitivity = StringComparison.OrdinalIgnoreCase;
|
||||
// if declarationItem matches the selected token, script SMO using that type
|
||||
|
||||
if (declarationItem.Title.Equals(tokenText, caseSensitivity))
|
||||
{
|
||||
return GetDefinitionUsingDeclarationType(declarationItem.Type, declarationItem.DatabaseQualifiedName, tokenText, schemaName);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// if no declarationItem matched the selected token, we try to find the type of the token using QuickInfo.Text
|
||||
string quickInfoText = GetQuickInfoForToken(parseResult, parserLine, parserColumn, metadataDisplayInfoProvider);
|
||||
return GetDefinitionUsingQuickInfoText(quickInfoText, tokenText, schemaName);
|
||||
}
|
||||
// no definition found
|
||||
return GetDefinitionErrorResult(SR.PeekDefinitionNoResultsError);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Script an object using the type extracted from quickInfo Text
|
||||
/// </summary>
|
||||
/// <param name="quickInfoText">the text from the quickInfo for the selected token</param>
|
||||
/// <param name="tokenText">The text of the selected token</param>
|
||||
/// <param name="schemaName">Schema name</param>
|
||||
/// <returns></returns>
|
||||
internal DefinitionResult GetDefinitionUsingQuickInfoText(string quickInfoText, string tokenText, string schemaName)
|
||||
{
|
||||
StringComparison caseSensitivity = StringComparison.OrdinalIgnoreCase;
|
||||
string tokenType = GetTokenTypeFromQuickInfo(quickInfoText, tokenText, caseSensitivity);
|
||||
if (tokenType != null)
|
||||
{
|
||||
if (sqlObjectTypesFromQuickInfo.ContainsKey(tokenType.ToLowerInvariant()))
|
||||
{
|
||||
// With SqlLogin authentication, the defaultSchema property throws an Exception when accessed.
|
||||
// This workaround ensures that a schema name is present by attempting
|
||||
// to get the schema name from the declaration item.
|
||||
// If all fails, the default schema name is assumed to be "dbo"
|
||||
if ((connectionInfo != null && connectionInfo.ConnectionDetails.AuthenticationType.Equals(Constants.SqlLoginAuthenticationType)) && string.IsNullOrEmpty(schemaName))
|
||||
{
|
||||
string fullObjectName = this.GetFullObjectNameFromQuickInfo(quickInfoText, tokenText, caseSensitivity);
|
||||
schemaName = this.GetSchemaFromDatabaseQualifiedName(fullObjectName, tokenText);
|
||||
}
|
||||
Location[] locations = GetSqlObjectDefinition(
|
||||
tokenText,
|
||||
schemaName,
|
||||
sqlObjectTypesFromQuickInfo[tokenType.ToLowerInvariant()]
|
||||
);
|
||||
DefinitionResult result = new DefinitionResult
|
||||
{
|
||||
IsErrorResult = this.error,
|
||||
Message = this.errorMessage,
|
||||
Locations = locations
|
||||
};
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If a type was found but is not in sqlScriptGettersFromQuickInfo, then the type is not supported
|
||||
return GetDefinitionErrorResult(SR.PeekDefinitionTypeNotSupportedError);
|
||||
}
|
||||
}
|
||||
// no definition found
|
||||
return GetDefinitionErrorResult(SR.PeekDefinitionNoResultsError);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Script a object using the type extracted from declarationItem
|
||||
/// </summary>
|
||||
/// <param name="declarationItem">The Declaration object that matched with the selected token</param>
|
||||
/// <param name="tokenText">The text of the selected token</param>
|
||||
/// <param name="schemaName">Schema name</param>
|
||||
/// <returns></returns>
|
||||
internal DefinitionResult GetDefinitionUsingDeclarationType(DeclarationType type, string databaseQualifiedName, string tokenText, string schemaName)
|
||||
{
|
||||
if (sqlObjectTypes.ContainsKey(type))
|
||||
{
|
||||
// With SqlLogin authentication, the defaultSchema property throws an Exception when accessed.
|
||||
// This workaround ensures that a schema name is present by attempting
|
||||
// to get the schema name from the declaration item.
|
||||
// If all fails, the default schema name is assumed to be "dbo"
|
||||
if ((connectionInfo != null && connectionInfo.ConnectionDetails.AuthenticationType.Equals(Constants.SqlLoginAuthenticationType)) && string.IsNullOrEmpty(schemaName))
|
||||
{
|
||||
string fullObjectName = databaseQualifiedName;
|
||||
schemaName = this.GetSchemaFromDatabaseQualifiedName(fullObjectName, tokenText);
|
||||
}
|
||||
Location[] locations = GetSqlObjectDefinition(
|
||||
tokenText,
|
||||
schemaName,
|
||||
sqlObjectTypes[type]
|
||||
);
|
||||
DefinitionResult result = new DefinitionResult
|
||||
{
|
||||
IsErrorResult = this.error,
|
||||
Message = this.errorMessage,
|
||||
Locations = locations
|
||||
};
|
||||
return result;
|
||||
}
|
||||
// If a type was found but is not in sqlScriptGetters, then the type is not supported
|
||||
return GetDefinitionErrorResult(SR.PeekDefinitionTypeNotSupportedError);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Script a object using SMO and write to a file.
|
||||
/// </summary>
|
||||
/// <param name="sqlScriptGetter">Function that returns the SMO scripts for an object</param>
|
||||
/// <param name="objectName">SQL object name</param>
|
||||
/// <param name="schemaName">Schema name or null</param>
|
||||
/// <param name="objectType">Type of SQL object</param>
|
||||
/// <returns>Location object representing URI and range of the script file</returns>
|
||||
internal Location[] GetSqlObjectDefinition(
|
||||
string objectName,
|
||||
string schemaName,
|
||||
string objectType)
|
||||
{
|
||||
// script file destination
|
||||
string tempFileName = (schemaName != null) ? Path.Combine(this.tempPath, string.Format("{0}.{1}.sql", schemaName, objectName))
|
||||
: Path.Combine(this.tempPath, string.Format("{0}.sql", objectName));
|
||||
|
||||
SmoScriptingOperation operation = InitScriptOperation(objectName, schemaName, objectType);
|
||||
operation.Execute();
|
||||
string script = operation.ScriptText;
|
||||
|
||||
bool objectFound = false;
|
||||
int createStatementLineNumber = 0;
|
||||
|
||||
File.WriteAllText(tempFileName, script);
|
||||
string[] lines = File.ReadAllLines(tempFileName);
|
||||
int lineCount = 0;
|
||||
string createSyntax = null;
|
||||
if (objectScriptMap.ContainsKey(objectType.ToLower()))
|
||||
{
|
||||
createSyntax = string.Format("CREATE");
|
||||
foreach (string line in lines)
|
||||
{
|
||||
if (LineContainsObject(line, objectName, createSyntax))
|
||||
{
|
||||
createStatementLineNumber = lineCount;
|
||||
objectFound = true;
|
||||
break;
|
||||
}
|
||||
lineCount++;
|
||||
}
|
||||
}
|
||||
if (objectFound)
|
||||
{
|
||||
Location[] locations = GetLocationFromFile(tempFileName, createStatementLineNumber);
|
||||
return locations;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.error = true;
|
||||
this.errorMessage = SR.PeekDefinitionNoResultsError;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#region Helper Methods
|
||||
/// <summary>
|
||||
/// Return schema name from the full name of the database. If schema is missing return dbo as schema name.
|
||||
/// </summary>
|
||||
/// <param name="fullObjectName"> The full database qualified name(database.schema.object)</param>
|
||||
/// <param name="objectName"> Object name</param>
|
||||
/// <returns>Schema name</returns>
|
||||
internal string GetSchemaFromDatabaseQualifiedName(string fullObjectName, string objectName)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(fullObjectName))
|
||||
{
|
||||
string[] tokens = fullObjectName.Split('.');
|
||||
for (int i = tokens.Length - 1; i > 0; i--)
|
||||
{
|
||||
if (tokens[i].Equals(objectName))
|
||||
{
|
||||
return tokens[i - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
return "dbo";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a file to a location array containing a location object as expected by the extension
|
||||
/// </summary>
|
||||
internal Location[] GetLocationFromFile(string tempFileName, int lineNumber)
|
||||
{
|
||||
// Get absolute Uri based on uri format. This works around a dotnetcore URI bug for linux paths.
|
||||
if (Path.DirectorySeparatorChar.Equals('/'))
|
||||
{
|
||||
tempFileName = "file:" + tempFileName;
|
||||
}
|
||||
else
|
||||
{
|
||||
tempFileName = new Uri(tempFileName).AbsoluteUri;
|
||||
}
|
||||
// Create a location array containing the tempFile Uri, as expected by VSCode.
|
||||
Location[] locations = new[]
|
||||
{
|
||||
new Location
|
||||
{
|
||||
Uri = tempFileName,
|
||||
Range = new Range
|
||||
{
|
||||
Start = new Position { Line = lineNumber, Character = 0},
|
||||
End = new Position { Line = lineNumber + 1, Character = 0}
|
||||
}
|
||||
}
|
||||
};
|
||||
return locations;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to create definition error result object
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">Error message</param>
|
||||
/// <returns> DefinitionResult</returns>
|
||||
internal DefinitionResult GetDefinitionErrorResult(string errorMessage)
|
||||
{
|
||||
return new DefinitionResult
|
||||
{
|
||||
IsErrorResult = true,
|
||||
Message = errorMessage,
|
||||
Locations = null
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return full object name(database.schema.objectName) from the quickInfo text("type database.schema.objectName")
|
||||
/// </summary>
|
||||
/// <param name="quickInfoText">QuickInfo Text for this token</param>
|
||||
/// <param name="tokenText">Token Text</param>
|
||||
/// <param name="caseSensitivity">StringComparison enum</param>
|
||||
/// <returns></returns>
|
||||
internal string GetFullObjectNameFromQuickInfo(string quickInfoText, string tokenText, StringComparison caseSensitivity)
|
||||
{
|
||||
if (string.IsNullOrEmpty(quickInfoText) || string.IsNullOrEmpty(tokenText))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// extract full object name from quickInfo text
|
||||
string[] tokens = quickInfoText.Split(' ');
|
||||
List<string> tokenList = tokens.Where(el => el.IndexOf(tokenText, caseSensitivity) >= 0).ToList();
|
||||
return (tokenList?.Count() > 0) ? tokenList[0] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return token type from the quickInfo text("type database.schema.objectName")
|
||||
/// </summary>
|
||||
/// <param name="quickInfoText">QuickInfo Text for this token</param>
|
||||
/// <param name="tokenText"Token Text></param>
|
||||
/// <param name="caseSensitivity">StringComparison enum</param>
|
||||
/// <returns></returns>
|
||||
internal string GetTokenTypeFromQuickInfo(string quickInfoText, string tokenText, StringComparison caseSensitivity)
|
||||
{
|
||||
if (string.IsNullOrEmpty(quickInfoText) || string.IsNullOrEmpty(tokenText))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// extract string denoting the token type from quickInfo text
|
||||
string[] tokens = quickInfoText.Split(' ');
|
||||
List<int> indexList = tokens.Select((s, i) => new { i, s }).Where(el => (el.s).IndexOf(tokenText, caseSensitivity) >= 0).Select(el => el.i).ToList();
|
||||
return (indexList?.Count() > 0) ? String.Join(" ", tokens.Take(indexList[0])) : null;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper method that calls Resolver.GetQuickInfo
|
||||
/// </summary>
|
||||
internal string GetQuickInfoForToken(ParseResult parseResult, int parserLine, int parserColumn, IMetadataDisplayInfoProvider metadataDisplayInfoProvider)
|
||||
{
|
||||
if (parseResult == null || metadataDisplayInfoProvider == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Babel.CodeObjectQuickInfo quickInfo = Resolver.GetQuickInfo(
|
||||
parseResult, parserLine, parserColumn, metadataDisplayInfoProvider);
|
||||
return quickInfo?.Text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper method that calls Resolver.FindCompletions
|
||||
/// </summary>
|
||||
/// <param name="parseResult"></param>
|
||||
/// <param name="parserLine"></param>
|
||||
/// <param name="parserColumn"></param>
|
||||
/// <param name="metadataDisplayInfoProvider"></param>
|
||||
/// <returns></returns>
|
||||
internal IEnumerable<Declaration> GetCompletionsForToken(ParseResult parseResult, int parserLine, int parserColumn, IMetadataDisplayInfoProvider metadataDisplayInfoProvider)
|
||||
{
|
||||
if (parseResult == null || metadataDisplayInfoProvider == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return Resolver.FindCompletions(
|
||||
parseResult, parserLine, parserColumn, metadataDisplayInfoProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper method that calls Resolver.FindCompletions
|
||||
/// </summary>
|
||||
/// <param name="objectName"></param>
|
||||
/// <param name="schemaName"></param>
|
||||
/// <param name="objectType"></param>
|
||||
/// <param name="tempFileName"></param>
|
||||
/// <returns></returns>
|
||||
internal SmoScriptingOperation InitScriptOperation(string objectName, string schemaName, string objectType)
|
||||
{
|
||||
// object that has to be scripted
|
||||
ScriptingObject scriptingObject = new ScriptingObject
|
||||
{
|
||||
Name = objectName,
|
||||
Schema = schemaName,
|
||||
Type = objectType
|
||||
};
|
||||
|
||||
// scripting options
|
||||
ScriptOptions options = new ScriptOptions
|
||||
{
|
||||
ScriptCreateDrop = "ScriptCreate",
|
||||
TypeOfDataToScript = "SchemaOnly",
|
||||
ScriptStatistics = "ScriptStatsNone",
|
||||
ScriptExtendedProperties = false,
|
||||
ScriptUseDatabase = false,
|
||||
IncludeIfNotExists = false,
|
||||
GenerateScriptForDependentObjects = false,
|
||||
IncludeDescriptiveHeaders = false,
|
||||
ScriptCheckConstraints = false,
|
||||
ScriptChangeTracking = false,
|
||||
ScriptDataCompressionOptions = false,
|
||||
ScriptForeignKeys = false,
|
||||
ScriptFullTextIndexes = false,
|
||||
ScriptIndexes = false,
|
||||
ScriptPrimaryKeys = false,
|
||||
ScriptTriggers = false,
|
||||
UniqueKeys = false
|
||||
|
||||
};
|
||||
|
||||
List<ScriptingObject> objectList = new List<ScriptingObject>();
|
||||
objectList.Add(scriptingObject);
|
||||
|
||||
// create parameters for the scripting operation
|
||||
|
||||
ScriptingParams parameters = new ScriptingParams
|
||||
{
|
||||
ConnectionString = ConnectionService.BuildConnectionString(this.connectionInfo.ConnectionDetails),
|
||||
ScriptingObjects = objectList,
|
||||
ScriptOptions = options,
|
||||
ScriptDestination = "ToEditor"
|
||||
};
|
||||
|
||||
return new ScriptAsScriptingOperation(parameters, DataSource);
|
||||
}
|
||||
|
||||
internal bool LineContainsObject(string line, string objectName, string createSyntax)
|
||||
{
|
||||
if (line.IndexOf(createSyntax, StringComparison.OrdinalIgnoreCase) >= 0 &&
|
||||
line.IndexOf(objectName, StringComparison.OrdinalIgnoreCase) >=0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static class ScriptingGlobals
|
||||
{
|
||||
/// <summary>
|
||||
/// Left delimiter for an named object
|
||||
/// </summary>
|
||||
public const char LeftDelimiter = '[';
|
||||
|
||||
/// <summary>
|
||||
/// right delimiter for a named object
|
||||
/// </summary>
|
||||
public const char RightDelimiter = ']';
|
||||
}
|
||||
|
||||
internal static class ScriptingUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Quote the name of a given sql object.
|
||||
/// </summary>
|
||||
/// <param name="sqlObject">object</param>
|
||||
/// <returns>quoted object name</returns>
|
||||
internal static string QuoteObjectName(string sqlObject)
|
||||
{
|
||||
return QuoteObjectName(sqlObject, ']');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quotes the name of a given sql object
|
||||
/// </summary>
|
||||
/// <param name="sqlObject">object</param>
|
||||
/// <param name="quote">quote to use</param>
|
||||
/// <returns></returns>
|
||||
internal static string QuoteObjectName(string sqlObject, char quote)
|
||||
{
|
||||
int len = sqlObject.Length;
|
||||
StringBuilder result = new StringBuilder(sqlObject.Length);
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
if (sqlObject[i] == quote)
|
||||
{
|
||||
result.Append(quote);
|
||||
}
|
||||
result.Append(sqlObject[i]);
|
||||
}
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
internal static string SelectAllValuesFromTransmissionQueue(Urn urn)
|
||||
{
|
||||
string script = string.Empty;
|
||||
StringBuilder selectQuery = new StringBuilder();
|
||||
|
||||
/*
|
||||
SELECT TOP *, casted_message_body =
|
||||
CASE MESSAGE_TYPE_NAME WHEN 'X'
|
||||
THEN CAST(MESSAGE_BODY AS NVARCHAR(MAX))
|
||||
ELSE MESSAGE_BODY
|
||||
END
|
||||
FROM [new].[sys].[transmission_queue]
|
||||
*/
|
||||
selectQuery.Append("SELECT TOP (1000) ");
|
||||
selectQuery.Append("*, casted_message_body = \r\nCASE message_type_name WHEN 'X' \r\n THEN CAST(message_body AS NVARCHAR(MAX)) \r\n ELSE message_body \r\nEND \r\n");
|
||||
|
||||
// from clause
|
||||
selectQuery.Append("FROM ");
|
||||
Urn dbUrn = urn;
|
||||
|
||||
// database
|
||||
while (dbUrn.Parent != null && dbUrn.Type != "Database")
|
||||
{
|
||||
dbUrn = dbUrn.Parent;
|
||||
}
|
||||
selectQuery.AppendFormat("{0}{1}{2}",
|
||||
ScriptingGlobals.LeftDelimiter,
|
||||
ScriptingUtils.QuoteObjectName(dbUrn.GetAttribute("Name"), ScriptingGlobals.RightDelimiter),
|
||||
ScriptingGlobals.RightDelimiter);
|
||||
//SYS
|
||||
selectQuery.AppendFormat(".{0}sys{1}",
|
||||
ScriptingGlobals.LeftDelimiter,
|
||||
ScriptingGlobals.RightDelimiter);
|
||||
//TRANSMISSION QUEUE
|
||||
selectQuery.AppendFormat(".{0}transmission_queue{1}",
|
||||
ScriptingGlobals.LeftDelimiter,
|
||||
ScriptingGlobals.RightDelimiter);
|
||||
|
||||
script = selectQuery.ToString();
|
||||
return script;
|
||||
}
|
||||
|
||||
internal static string SelectAllValues(Urn urn)
|
||||
{
|
||||
string script = string.Empty;
|
||||
StringBuilder selectQuery = new StringBuilder();
|
||||
selectQuery.Append("SELECT TOP (1000) ");
|
||||
selectQuery.Append("*, casted_message_body = \r\nCASE message_type_name WHEN 'X' \r\n THEN CAST(message_body AS NVARCHAR(MAX)) \r\n ELSE message_body \r\nEND \r\n");
|
||||
|
||||
// from clause
|
||||
selectQuery.Append("FROM ");
|
||||
Urn dbUrn = urn;
|
||||
|
||||
// database
|
||||
while (dbUrn.Parent != null && dbUrn.Type != "Database")
|
||||
{
|
||||
dbUrn = dbUrn.Parent;
|
||||
}
|
||||
selectQuery.AppendFormat("{0}{1}{2}",
|
||||
ScriptingGlobals.LeftDelimiter,
|
||||
ScriptingUtils.QuoteObjectName(dbUrn.GetAttribute("Name"), ScriptingGlobals.RightDelimiter),
|
||||
ScriptingGlobals.RightDelimiter);
|
||||
// schema
|
||||
selectQuery.AppendFormat(".{0}{1}{2}",
|
||||
ScriptingGlobals.LeftDelimiter,
|
||||
ScriptingUtils.QuoteObjectName(urn.GetAttribute("Schema"), ScriptingGlobals.RightDelimiter),
|
||||
ScriptingGlobals.RightDelimiter);
|
||||
// object
|
||||
selectQuery.AppendFormat(".{0}{1}{2}",
|
||||
ScriptingGlobals.LeftDelimiter,
|
||||
ScriptingUtils.QuoteObjectName(urn.GetAttribute("Name"), ScriptingGlobals.RightDelimiter),
|
||||
ScriptingGlobals.RightDelimiter);
|
||||
|
||||
//Adding no lock in the end.
|
||||
selectQuery.AppendFormat(" WITH(NOLOCK)");
|
||||
|
||||
script = selectQuery.ToString();
|
||||
return script;
|
||||
}
|
||||
|
||||
internal DataTable GetColumnNames(Server server, Urn urn, bool isDw)
|
||||
{
|
||||
List<string> filterExpressions = new List<string>();
|
||||
if (server.Version.Major >= 10)
|
||||
{
|
||||
// We don't have to include sparce columns as all the sparce columns data.
|
||||
// Can be obtain from column set columns.
|
||||
filterExpressions.Add("@IsSparse=0");
|
||||
}
|
||||
|
||||
// Check if we're called for EDIT for SQL2016+/Sterling+.
|
||||
// We need to omit temporal columns if such are present on this table.
|
||||
if (server.Version.Major >= 13 || (DatabaseEngineType.SqlAzureDatabase == server.DatabaseEngineType && server.Version.Major >= 12))
|
||||
{
|
||||
// We're called in order to generate a list of columns for EDIT TOP N rows.
|
||||
// Don't return auto-generated, auto-populated, read-only temporal columns.
|
||||
filterExpressions.Add("@GeneratedAlwaysType=0");
|
||||
}
|
||||
|
||||
// Check if we're called for SQL2017/Sterling+.
|
||||
// We need to omit graph internal columns if such are present on this table.
|
||||
if (server.Version.Major >= 14 || (DatabaseEngineType.SqlAzureDatabase == server.DatabaseEngineType && !isDw))
|
||||
{
|
||||
// from Smo.GraphType:
|
||||
// 0 = None
|
||||
// 1 = GraphId
|
||||
// 2 = GraphIdComputed
|
||||
// 3 = GraphFromId
|
||||
// 4 = GraphFromObjId
|
||||
// 5 = GraphFromIdComputed
|
||||
// 6 = GraphToId
|
||||
// 7 = GraphToObjId
|
||||
// 8 = GraphToIdComputed
|
||||
//
|
||||
// We only want to show types 0, 2, 5, and 8:
|
||||
filterExpressions.Add("(@GraphType=0 or @GraphType=2 or @GraphType=5 or @GraphType=8)");
|
||||
}
|
||||
|
||||
Request request = new Request();
|
||||
// If we have any filters on the columns, add them.
|
||||
if (filterExpressions.Count > 0)
|
||||
{
|
||||
request.Urn = String.Format("{0}/Column[{1}]", urn.ToString(), string.Join(" and ", filterExpressions.ToArray()));
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Urn = String.Format("{0}/Column", urn.ToString());
|
||||
}
|
||||
|
||||
request.Fields = new String[] { "Name" };
|
||||
|
||||
// get the columns in the order they were created
|
||||
OrderBy order = new OrderBy();
|
||||
order.Dir = OrderBy.Direction.Asc;
|
||||
order.Field = "ID";
|
||||
request.OrderByList = new OrderBy[] { order };
|
||||
|
||||
Enumerator en = new Enumerator();
|
||||
|
||||
// perform the query.
|
||||
DataTable dt = null;
|
||||
EnumResult result = en.Process(server.ConnectionContext, request);
|
||||
|
||||
if (result.Type == ResultType.DataTable)
|
||||
{
|
||||
dt = result;
|
||||
}
|
||||
else
|
||||
{
|
||||
dt = ((DataSet)result).Tables[0];
|
||||
}
|
||||
return dt;
|
||||
}
|
||||
|
||||
internal string SelectFromTableOrView(IDataSource dataSource, Urn urn)
|
||||
{
|
||||
StringBuilder selectQuery = new StringBuilder();
|
||||
|
||||
// TODOKusto: Can we combine this with snippets. All queries generated here could also be snippets.
|
||||
// TODOKusto: Extract into the Kusto folder.
|
||||
selectQuery.Append($"{KustoQueryUtils.EscapeName(urn.GetAttribute("Name"))}");
|
||||
selectQuery.Append($"{KustoQueryUtils.StatementSeparator}");
|
||||
selectQuery.Append("limit 1000");
|
||||
|
||||
return selectQuery.ToString();
|
||||
}
|
||||
|
||||
internal string AlterFunction(IDataSource dataSource, ScriptingObject scriptingObject)
|
||||
{
|
||||
var functionName = scriptingObject.Name.Substring(0, scriptingObject.Name.IndexOf('('));
|
||||
return dataSource.GenerateAlterFunctionScript(functionName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using System.Collections.Generic;
|
||||
using System.Data.SqlClient;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.Kusto.ServiceLayer.DataSource;
|
||||
using Microsoft.SqlServer.Management.SqlScriptPublish;
|
||||
using Microsoft.Kusto.ServiceLayer.Scripting.Contracts;
|
||||
using Microsoft.SqlTools.Utility;
|
||||
@@ -28,7 +29,7 @@ namespace Microsoft.Kusto.ServiceLayer.Scripting
|
||||
|
||||
private string azureAccessToken;
|
||||
|
||||
public ScriptingScriptOperation(ScriptingParams parameters, string azureAccessToken): base(parameters)
|
||||
public ScriptingScriptOperation(ScriptingParams parameters, string azureAccessToken) : base(parameters)
|
||||
{
|
||||
this.azureAccessToken = azureAccessToken;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.Hosting.Protocol;
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
using Microsoft.Kusto.ServiceLayer.Connection;
|
||||
using Microsoft.Kusto.ServiceLayer.Hosting;
|
||||
using Microsoft.Kusto.ServiceLayer.DataSource;
|
||||
using Microsoft.Kusto.ServiceLayer.Scripting.Contracts;
|
||||
using Microsoft.SqlTools.Utility;
|
||||
using Microsoft.Kusto.ServiceLayer.Utility;
|
||||
@@ -21,19 +21,21 @@ namespace Microsoft.Kusto.ServiceLayer.Scripting
|
||||
/// Main class for Scripting Service functionality
|
||||
/// </summary>
|
||||
public sealed class ScriptingService : IDisposable
|
||||
{
|
||||
{
|
||||
private const int ScriptingOperationTimeout = 60000;
|
||||
|
||||
private static readonly Lazy<ScriptingService> LazyInstance = new Lazy<ScriptingService>(() => new ScriptingService());
|
||||
|
||||
public static ScriptingService Instance => LazyInstance.Value;
|
||||
|
||||
private static ConnectionService connectionService = null;
|
||||
private static ConnectionService connectionService;
|
||||
|
||||
private readonly Lazy<ConcurrentDictionary<string, ScriptingOperation>> operations =
|
||||
new Lazy<ConcurrentDictionary<string, ScriptingOperation>>(() => new ConcurrentDictionary<string, ScriptingOperation>());
|
||||
|
||||
private bool disposed;
|
||||
|
||||
private IScripter _scripter;
|
||||
|
||||
/// <summary>
|
||||
/// Internal for testing purposes only
|
||||
@@ -64,8 +66,9 @@ namespace Microsoft.Kusto.ServiceLayer.Scripting
|
||||
/// </summary>
|
||||
/// <param name="serviceHost"></param>
|
||||
/// <param name="context"></param>
|
||||
public void InitializeService(ServiceHost serviceHost)
|
||||
public void InitializeService(ServiceHost serviceHost, IScripter scripter)
|
||||
{
|
||||
_scripter = scripter;
|
||||
serviceHost.SetRequestHandler(ScriptingRequest.Type, this.HandleScriptExecuteRequest);
|
||||
serviceHost.SetRequestHandler(ScriptingCancelRequest.Type, this.HandleScriptCancelRequest);
|
||||
serviceHost.SetRequestHandler(ScriptingListObjectsRequest.Type, this.HandleListObjectsRequest);
|
||||
@@ -132,7 +135,7 @@ namespace Microsoft.Kusto.ServiceLayer.Scripting
|
||||
}
|
||||
else
|
||||
{
|
||||
operation = new ScriptAsScriptingOperation(parameters, accessToken);
|
||||
operation = new ScriptAsScriptingOperation(parameters, accessToken, _scripter);
|
||||
}
|
||||
|
||||
operation.PlanNotification += (sender, e) => requestContext.SendEvent(ScriptingPlanNotificationEvent.Type, e).Wait();
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Microsoft.Kusto.ServiceLayer.Scripting
|
||||
/// </summary>
|
||||
public abstract class SmoScriptingOperation : ScriptingOperation
|
||||
{
|
||||
private bool disposed = false;
|
||||
private bool _disposed;
|
||||
|
||||
public SmoScriptingOperation(ScriptingParams parameters)
|
||||
{
|
||||
@@ -177,10 +177,10 @@ namespace Microsoft.Kusto.ServiceLayer.Scripting
|
||||
/// </summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
if (!disposed)
|
||||
if (!_disposed)
|
||||
{
|
||||
this.Cancel();
|
||||
disposed = true;
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user