mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-23 01:25:42 -05:00
User management support classes (#1856)
* WIP * Fix nullable warnings in UserData class * WIP2 * WIP * Refresh database prototype classes * Fix some typos & merge issues * WIP * WIP * WIP * Additional updates * Remove unneded using
This commit is contained in:
@@ -3,11 +3,11 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security;
|
||||
@@ -30,7 +30,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
{
|
||||
SQL,
|
||||
OLAP, //This type is used only for non-express sku
|
||||
SQLCE,
|
||||
SQLCE,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
@@ -38,32 +38,31 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
|
||||
#region Fields
|
||||
|
||||
private ServerConnection serverConnection;
|
||||
private Server m_server = null;
|
||||
protected XmlDocument m_doc = null;
|
||||
private XmlDocument originalDocument = null;
|
||||
private SqlOlapConnectionInfoBase connectionInfo = null;
|
||||
private SqlConnectionInfoWithConnection sqlCiWithConnection;
|
||||
private ServerConnection? serverConnection;
|
||||
private Server? m_server;
|
||||
protected XmlDocument? m_doc;
|
||||
private XmlDocument? originalDocument;
|
||||
private SqlOlapConnectionInfoBase? connectionInfo;
|
||||
private SqlConnectionInfoWithConnection? sqlCiWithConnection;
|
||||
private bool ownConnection = true;
|
||||
private IManagedConnection managedConnection;
|
||||
protected string serverName;
|
||||
private IManagedConnection? managedConnection;
|
||||
protected string? serverName;
|
||||
|
||||
//This member is used for non-express sku only
|
||||
protected string olapServerName;
|
||||
protected string? olapServerName;
|
||||
|
||||
protected string sqlceFilename;
|
||||
protected string? sqlceFilename;
|
||||
|
||||
private ServerType serverType = ServerType.UNKNOWN;
|
||||
|
||||
private Hashtable m_hashTable = null;
|
||||
private Hashtable? m_hashTable;
|
||||
|
||||
private string objectNameKey = "object-name-9524b5c1-e996-4119-a433-b5b947985566";
|
||||
private string objectSchemaKey = "object-schema-ccaf2efe-8fa3-4f62-be79-62ef3cbe7390";
|
||||
|
||||
private SqlSmoObject sqlDialogSubject = null;
|
||||
private SqlSmoObject? sqlDialogSubject;
|
||||
|
||||
private int sqlServerVersion = 0;
|
||||
private int sqlServerEffectiveVersion = 0;
|
||||
|
||||
|
||||
#endregion
|
||||
@@ -73,7 +72,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
/// <summary>
|
||||
/// gets/sets XmlDocument with parameters
|
||||
/// </summary>
|
||||
public XmlDocument Document
|
||||
public XmlDocument? Document
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -85,8 +84,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
//this.originalDocument = (XmlDocument) value.Clone();
|
||||
this.originalDocument = value;
|
||||
this.originalDocument = (XmlDocument)value.Clone();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -111,7 +109,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
/// <summary>
|
||||
/// gets/sets SMO server object
|
||||
/// </summary>
|
||||
public Server Server
|
||||
public Server? Server
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -127,26 +125,29 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
/// <summary>
|
||||
/// connection info that should be used by the dialogs
|
||||
/// </summary>
|
||||
public SqlOlapConnectionInfoBase ConnectionInfo
|
||||
public SqlOlapConnectionInfoBase? ConnectionInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
//// update the database name in the serverconnection object to set the correct database context when connected to Azure
|
||||
//var conn = this.connectionInfo as SqlConnectionInfoWithConnection;
|
||||
// update the database name in the serverconnection object to set the correct database context when connected to Azure
|
||||
var conn = this.connectionInfo as SqlConnectionInfoWithConnection;
|
||||
|
||||
//if (conn != null && conn.ServerConnection.DatabaseEngineType == DatabaseEngineType.SqlAzureDatabase)
|
||||
//{
|
||||
// if (this.RelevantDatabaseName != null)
|
||||
// {
|
||||
// IComparer<string> dbNamesComparer = ServerConnection.ConnectionFactory.GetInstance(conn.ServerConnection).ServerComparer as IComparer<string>;
|
||||
// if (dbNamesComparer.Compare(this.RelevantDatabaseName, conn.DatabaseName) != 0)
|
||||
// {
|
||||
// ServerConnection serverConnection = conn.ServerConnection.GetDatabaseConnection(this.RelevantDatabaseName, true, conn.AccessToken);
|
||||
// ((SqlConnectionInfoWithConnection)this.connectionInfo).ServerConnection = serverConnection;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// Don't update the database name if this is a Gen3 connection since Gen3 supports USE from the server connection.
|
||||
if (conn != null &&
|
||||
conn.ServerConnection.DatabaseEngineType == DatabaseEngineType.SqlAzureDatabase &&
|
||||
!(conn.ServerConnection.DatabaseEngineEdition == DatabaseEngineEdition.SqlDataWarehouse &&
|
||||
conn.ServerConnection.ProductVersion.Major >= 12))
|
||||
{
|
||||
if (this.RelevantDatabaseName != null)
|
||||
{
|
||||
IComparer<string> dbNamesComparer = new ServerComparer(conn.ServerConnection, "master");
|
||||
if (dbNamesComparer.Compare(this.RelevantDatabaseName, conn.DatabaseName) != 0 && this.connectionInfo != null)
|
||||
{
|
||||
ServerConnection databaseConnection = conn.ServerConnection.GetDatabaseConnection(this.RelevantDatabaseName, true, conn.AccessToken);
|
||||
((SqlConnectionInfoWithConnection)this.connectionInfo).ServerConnection = databaseConnection;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.connectionInfo;
|
||||
}
|
||||
}
|
||||
@@ -163,11 +164,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
{
|
||||
if (this.serverType != ServerType.SQL)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(false, "CDataContainer.ServerConnection can be used only for SQL connection");
|
||||
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
if (this.connectionInfo == null)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(false, "CDataContainer.ServerConnection can be used only after ConnectionInfo property has been set");
|
||||
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
@@ -177,12 +182,13 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
}
|
||||
else
|
||||
{
|
||||
SqlConnectionInfo sci = this.connectionInfo as SqlConnectionInfo;
|
||||
SqlConnectionInfo? sci = this.connectionInfo as SqlConnectionInfo;
|
||||
System.Diagnostics.Debug.Assert(sci != null, "CDataContainer.ServerConnection: connection info MUST be SqlConnectionInfo");
|
||||
this.serverConnection = new ServerConnection(sci);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
System.Diagnostics.Debug.Assert(this.serverConnection != null);
|
||||
return this.serverConnection;
|
||||
}
|
||||
}
|
||||
@@ -191,7 +197,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
/// returns SMO server connection object constructed off the connectionInfo.
|
||||
/// This method cannot work until ConnectionInfo property has been set
|
||||
/// </summary>
|
||||
public SqlConnectionInfoWithConnection SqlInfoWithConnection
|
||||
public SqlConnectionInfoWithConnection? SqlInfoWithConnection
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -199,11 +205,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
{
|
||||
if (this.serverType != ServerType.SQL)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(false, "CDataContainer.ServerConnection can be used only for SQL connection");
|
||||
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
if (this.connectionInfo == null)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(false, "CDataContainer.ServerConnection can be used only after ConnectionInfo property has been set");
|
||||
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
@@ -214,16 +224,18 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
}
|
||||
else
|
||||
{
|
||||
SqlConnectionInfo sci = this.connectionInfo as SqlConnectionInfo;
|
||||
SqlConnectionInfo? sci = this.connectionInfo as SqlConnectionInfo;
|
||||
System.Diagnostics.Debug.Assert(sci != null, "CDataContainer.ServerConnection: connection info MUST be SqlConnectionInfo");
|
||||
this.serverConnection = new ServerConnection(sci);
|
||||
}
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.Assert(this.serverConnection != null);
|
||||
return this.sqlCiWithConnection;
|
||||
}
|
||||
}
|
||||
|
||||
public string ServerName
|
||||
public string? ServerName
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -247,7 +259,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
}
|
||||
}
|
||||
|
||||
public string SqlCeFileName
|
||||
public string? SqlCeFileName
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -260,7 +272,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
}
|
||||
|
||||
//This member is used for non-express sku only
|
||||
public string OlapServerName
|
||||
public string? OlapServerName
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -418,11 +430,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
/// <summary>
|
||||
/// The SQL SMO object that is the subject of the dialog.
|
||||
/// </summary>
|
||||
public SqlSmoObject SqlDialogSubject
|
||||
public SqlSmoObject? SqlDialogSubject
|
||||
{
|
||||
get
|
||||
{
|
||||
SqlSmoObject result = null;
|
||||
SqlSmoObject? result;
|
||||
|
||||
if (this.sqlDialogSubject != null)
|
||||
{
|
||||
@@ -430,7 +442,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
}
|
||||
else
|
||||
{
|
||||
result = this.Server.GetSmoObject(this.ObjectUrn);
|
||||
result = this.Server?.GetSmoObject(this.ObjectUrn);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -451,6 +463,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
System.Diagnostics.Debug.Assert(this.Server != null, "SMO Server object is null!");
|
||||
System.Diagnostics.Debug.Assert(this.Server.ConnectionContext != null, "SMO Server Connection object is null!");
|
||||
|
||||
if (this.Server != null && this.Server.ConnectionContext != null)
|
||||
{
|
||||
result = this.Server.ConnectionContext.IsInFixedServerRole(FixedServerRoles.SysAdmin);
|
||||
@@ -471,6 +486,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
string result = String.Empty;
|
||||
string urnText = this.GetDocumentPropertyString("urn");
|
||||
|
||||
System.Diagnostics.Debug.Assert(urnText.Length != 0, "couldn't get relevant URN");
|
||||
|
||||
if (urnText.Length != 0)
|
||||
{
|
||||
Urn urn = new Urn(urnText);
|
||||
@@ -501,6 +518,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
{
|
||||
this.sqlServerVersion = 9;
|
||||
|
||||
System.Diagnostics.Debug.Assert(this.ConnectionInfo != null, "ConnectionInfo is null!");
|
||||
System.Diagnostics.Debug.Assert(ServerType.SQL == this.ContainerServerType, "unexpected server type");
|
||||
|
||||
if ((this.ConnectionInfo != null) && (ServerType.SQL == this.ContainerServerType))
|
||||
{
|
||||
Enumerator enumerator = new Enumerator();
|
||||
@@ -520,99 +540,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The server version the database is emulating. If database compatibility level is
|
||||
/// not relevant to the subject, then this just returns the actual server version.
|
||||
/// </summary>
|
||||
public int EffectiveSqlServerVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.sqlServerEffectiveVersion == 0)
|
||||
{
|
||||
this.sqlServerEffectiveVersion = 9;
|
||||
|
||||
if ((this.ConnectionInfo != null) && (ServerType.SQL == this.ContainerServerType))
|
||||
{
|
||||
string databaseName = this.RelevantDatabaseName;
|
||||
|
||||
if (databaseName.Length != 0)
|
||||
{
|
||||
Enumerator enumerator = new Enumerator();
|
||||
Urn urn = String.Format("Server/Database[@Name='{0}']", Urn.EscapeString(databaseName));
|
||||
string[] fields = new string[] { "CompatibilityLevel" };
|
||||
DataTable dataTable = enumerator.Process(this.ConnectionInfo, new Request(urn, fields));
|
||||
|
||||
if (dataTable.Rows.Count != 0)
|
||||
{
|
||||
|
||||
CompatibilityLevel level = (CompatibilityLevel)dataTable.Rows[0][0];
|
||||
|
||||
switch (level)
|
||||
{
|
||||
case CompatibilityLevel.Version60:
|
||||
case CompatibilityLevel.Version65:
|
||||
|
||||
this.sqlServerEffectiveVersion = 6;
|
||||
break;
|
||||
|
||||
case CompatibilityLevel.Version70:
|
||||
|
||||
this.sqlServerEffectiveVersion = 7;
|
||||
break;
|
||||
|
||||
case CompatibilityLevel.Version80:
|
||||
|
||||
this.sqlServerEffectiveVersion = 8;
|
||||
break;
|
||||
|
||||
case CompatibilityLevel.Version90:
|
||||
|
||||
this.sqlServerEffectiveVersion = 9;
|
||||
break;
|
||||
case CompatibilityLevel.Version100:
|
||||
|
||||
this.sqlServerEffectiveVersion = 10;
|
||||
break;
|
||||
case CompatibilityLevel.Version110:
|
||||
|
||||
this.sqlServerEffectiveVersion = 11;
|
||||
break;
|
||||
case CompatibilityLevel.Version120:
|
||||
|
||||
this.sqlServerEffectiveVersion = 12;
|
||||
break;
|
||||
|
||||
case CompatibilityLevel.Version130:
|
||||
this.sqlServerEffectiveVersion = 13;
|
||||
break;
|
||||
|
||||
case CompatibilityLevel.Version140:
|
||||
this.sqlServerEffectiveVersion = 14;
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
this.sqlServerEffectiveVersion = 14;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.sqlServerEffectiveVersion = this.SqlServerVersion;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.sqlServerEffectiveVersion = this.SqlServerVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.sqlServerEffectiveVersion;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors, finalizer
|
||||
@@ -628,9 +555,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
/// <param name="ciObj">connection info containing live connection</param>
|
||||
public CDataContainer(object ciObj, bool ownConnection)
|
||||
{
|
||||
SqlConnectionInfoWithConnection ci = (SqlConnectionInfoWithConnection)ciObj;
|
||||
SqlConnectionInfoWithConnection ci = (SqlConnectionInfoWithConnection)ciObj;
|
||||
if (ci == null)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(false, "CDataContainer.CDataContainer(SqlConnectionInfoWithConnection): specified connection info is null");
|
||||
|
||||
throw new ArgumentNullException("ci");
|
||||
}
|
||||
ApplyConnectionInfo(ci, ownConnection);
|
||||
@@ -645,9 +574,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
/// <param name="ci">connection info containing live connection</param>
|
||||
public CDataContainer(ServerType serverType, object ciObj, bool ownConnection)
|
||||
{
|
||||
SqlConnectionInfoWithConnection ci = (SqlConnectionInfoWithConnection)ciObj;
|
||||
SqlConnectionInfoWithConnection ci = (SqlConnectionInfoWithConnection)ciObj;
|
||||
if (ci == null)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(false, "CDataContainer.CDataContainer(SqlConnectionInfoWithConnection): specified connection info is null");
|
||||
|
||||
throw new ArgumentNullException("ci");
|
||||
}
|
||||
|
||||
@@ -656,12 +587,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
|
||||
if (serverType == ServerType.SQL)
|
||||
{
|
||||
//NOTE: ServerConnection property will constuct the object if needed
|
||||
m_server = new Server(ServerConnection);
|
||||
}
|
||||
//NOTE: ServerConnection property will construct the object if needed
|
||||
m_server = new Server(ServerConnection);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException(SR.UnknownServerType(serverType.ToString()));
|
||||
throw new ArgumentException(SR.UnknownServerType(serverType.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -674,7 +605,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
/// <param name="userName">User name for not trusted connections</param>
|
||||
/// <param name="password">Password for not trusted connections</param>
|
||||
/// <param name="xmlParameters">XML string with parameters</param>
|
||||
public CDataContainer(ServerType serverType, string serverName, bool trusted, string userName, SecureString password, string databaseName, string xmlParameters, string azureAccountToken = null)
|
||||
public CDataContainer(ServerType serverType, string serverName, bool trusted, string userName, SecureString password, string databaseName, string xmlParameters, string? azureAccountToken = null)
|
||||
{
|
||||
this.serverType = serverType;
|
||||
this.serverName = serverName;
|
||||
@@ -710,6 +641,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
/// <param name="xmlParameters">XML string with parameters</param>
|
||||
public CDataContainer(CDataContainer dataContainer, string xmlParameters)
|
||||
{
|
||||
//BUGBUG - should we be reusing same SqlConnectionInfoWithConnection if it is available?
|
||||
|
||||
System.Diagnostics.Debug.Assert(dataContainer.Server != null, "DataContainer.Server can not be null.");
|
||||
Server = dataContainer.Server;
|
||||
this.serverName = dataContainer.serverName;
|
||||
this.serverType = dataContainer.serverType;
|
||||
@@ -718,11 +652,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
|
||||
this.sqlCiWithConnection = dataContainer.connectionInfo as SqlConnectionInfoWithConnection;
|
||||
if (this.sqlCiWithConnection != null)
|
||||
{
|
||||
{
|
||||
//we want to be notified if it is closed
|
||||
this.sqlCiWithConnection.ConnectionClosed += new EventHandler(OnSqlConnectionClosed);
|
||||
}
|
||||
|
||||
if (this.connectionInfo is SqlConnectionInfo)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(this.sqlCiWithConnection != null, "CDataContainer.ConnectionInfo setter: for SQL connection info you MUST use SqlConnectionInfoWithConnection derived class!");
|
||||
}
|
||||
|
||||
if (xmlParameters != null)
|
||||
{
|
||||
XmlDocument doc = GenerateXmlDocumentFromString(xmlParameters);
|
||||
@@ -764,20 +703,24 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
|
||||
if (site != null)
|
||||
{
|
||||
// see if service provider supports INodeInformation interface from the object explorer
|
||||
// NOTE: we're trying to forcefully set connection information on the data container.
|
||||
// If this code doesn't execute, then dc.Init call below will result in CDataContainer
|
||||
// initializing its ConnectionInfo member with a new object contructed off the parameters
|
||||
// in the XML doc [server name, user name etc]
|
||||
IManagedConnection managedConnection = site.GetService(typeof(IManagedConnection)) as IManagedConnection;
|
||||
Trace.TraceInformation("CDataContainer.Init has non-null IServiceProvider");
|
||||
//see if service provider supports IManagedConnection interface from the object explorer
|
||||
|
||||
//NOTE: we're trying to forcefully set connection information on the data container.
|
||||
//If this code doesn't execute, then dc.Init call below will result in CDataContainer
|
||||
//initializing its ConnectionInfo member with a new object contructed off the parameters
|
||||
//in the XML doc [server name, user name etc]
|
||||
IManagedConnection? managedConnection =
|
||||
site.GetService(typeof(IManagedConnection)) as IManagedConnection;
|
||||
if (managedConnection != null)
|
||||
{
|
||||
Trace.TraceInformation("CDataContainer.Init has non-null IManagedConnection");
|
||||
this.SetManagedConnection(managedConnection);
|
||||
}
|
||||
}
|
||||
|
||||
this.Document = doc;
|
||||
LoadData();
|
||||
LoadData();
|
||||
|
||||
// finish the initialization
|
||||
this.Init(doc);
|
||||
@@ -808,13 +751,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
|
||||
// NOTE: ServerConnection property will constuct the object if needed
|
||||
m_server ??= new Server(ServerConnection);
|
||||
}
|
||||
else if (this.serverType == ServerType.SQLCE)
|
||||
{
|
||||
// do nothing; originally we were only distinguishing between two
|
||||
// types of servers (OLAP/SQL); as a result for SQLCE we were
|
||||
// executing the same codepath as for OLAP server which was
|
||||
// resulting in an exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -837,21 +773,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
|
||||
if (!bStatus || this.serverName.Length == 0)
|
||||
{
|
||||
if (this.sqlCiWithConnection != null)
|
||||
{
|
||||
bStatus = param.GetParam("database", ref this.sqlceFilename);
|
||||
if (bStatus && !string.IsNullOrEmpty(this.sqlceFilename))
|
||||
{
|
||||
this.serverType = ServerType.SQLCE;
|
||||
}
|
||||
else if (this.sqlCiWithConnection != null)
|
||||
{
|
||||
this.serverType = ServerType.SQL;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.serverType = ServerType.UNKNOWN;
|
||||
}
|
||||
}
|
||||
this.serverType = ServerType.SQL;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.serverType = ServerType.UNKNOWN;
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -877,10 +807,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
}
|
||||
|
||||
// Ensure there is no password in the XML document
|
||||
string temp = string.Empty;
|
||||
string? temp = string.Empty;
|
||||
if (param.GetParam("password", ref temp))
|
||||
{
|
||||
temp = null;
|
||||
temp = null;
|
||||
System.Diagnostics.Debug.Assert(false, "Plaintext password found in XML document! This must be fixed!");
|
||||
|
||||
throw new SecurityException();
|
||||
}
|
||||
|
||||
@@ -899,7 +831,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
/// </summary>
|
||||
/// <param name="managedConnection"></param>
|
||||
internal void SetManagedConnection(IManagedConnection managedConnection)
|
||||
{
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(this.managedConnection == null, "CDataContainer.SetManagedConnection: overwriting the previous value");
|
||||
this.managedConnection = managedConnection;
|
||||
|
||||
ApplyConnectionInfo(managedConnection.Connection, true);//it will do some extra initialization
|
||||
@@ -912,7 +845,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
/// <returns>The property value</returns>
|
||||
public object GetDocumentPropertyValue(string propertyName)
|
||||
{
|
||||
object result = null;
|
||||
object? result = null;
|
||||
STParameters param = new STParameters(this.Document);
|
||||
|
||||
param.GetBaseParam(propertyName, ref result);
|
||||
@@ -974,6 +907,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
/// </summary>
|
||||
private void InitializeObjectNameAndSchema()
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(ServerType.SQL == this.serverType, "This method only valid for SQL Servers");
|
||||
|
||||
string documentUrn = this.GetDocumentPropertyString("urn");
|
||||
if (documentUrn.Length != 0)
|
||||
{
|
||||
@@ -1003,8 +938,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
string userName,
|
||||
SecureString password,
|
||||
string databaseName,
|
||||
string azureAccountToken)
|
||||
{
|
||||
string? azureAccountToken)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(this.serverType == ServerType.SQL, "GetTempSqlConnectionInfoWithConnection should only be called for SQL Server type");
|
||||
|
||||
SqlConnectionInfoWithConnection tempCI = new SqlConnectionInfoWithConnection(serverName);
|
||||
tempCI.SingleConnection = false;
|
||||
tempCI.Pooled = false;
|
||||
@@ -1031,7 +968,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void OnSqlConnectionClosed(object sender, EventArgs e)
|
||||
private void OnSqlConnectionClosed(object? sender, EventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1042,7 +979,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
/// <param name="ci"></param>
|
||||
private void ApplyConnectionInfo(SqlOlapConnectionInfoBase ci, bool ownConnection)
|
||||
{
|
||||
|
||||
System.Diagnostics.Debug.Assert(this.connectionInfo == null, "CDataContainer.ApplyConnectionInfo: overwriting non-null connection info!");
|
||||
System.Diagnostics.Debug.Assert(ci != null, "CDataContainer.ApplyConnectionInfo: ci is null!");
|
||||
|
||||
this.connectionInfo = ci;
|
||||
this.ownConnection = ownConnection;
|
||||
|
||||
@@ -1050,12 +989,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
this.sqlCiWithConnection = ci as SqlConnectionInfoWithConnection;
|
||||
|
||||
if (this.sqlCiWithConnection != null)
|
||||
{
|
||||
{
|
||||
// we want to be notified if it is closed
|
||||
this.sqlCiWithConnection.ConnectionClosed += new EventHandler(OnSqlConnectionClosed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static bool MustRethrow(Exception exception)
|
||||
{
|
||||
bool result = false;
|
||||
@@ -1132,7 +1071,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
/// MUST be called, as we'll be closing SQL connection inside this call
|
||||
/// </summary>
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
{
|
||||
try
|
||||
{
|
||||
//take care of live SQL connection
|
||||
@@ -1175,42 +1114,56 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
this.managedConnection.Close();
|
||||
}
|
||||
this.managedConnection = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Create a data container object
|
||||
/// </summary>
|
||||
/// <param name="connInfo">connection info</param>
|
||||
/// <param name="databaseExists">flag indicating whether to create taskhelper for existing database or not</param>
|
||||
internal static CDataContainer CreateDataContainer(
|
||||
ConnectionInfo connInfo,
|
||||
ConnectionInfo connInfo,
|
||||
bool databaseExists = false,
|
||||
XmlDocument containerDoc = null)
|
||||
XmlDocument? containerDoc = null)
|
||||
{
|
||||
containerDoc ??= CreateDataContainerDocument(connInfo, databaseExists);
|
||||
|
||||
var serverConnection = ConnectionService.OpenServerConnection(connInfo, "DataContainer");
|
||||
|
||||
var connectionInfoWithConnection = new SqlConnectionInfoWithConnection();
|
||||
connectionInfoWithConnection.ServerConnection = serverConnection;
|
||||
|
||||
return CreateDataContainer(connectionInfoWithConnection, containerDoc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a data container object
|
||||
/// </summary>
|
||||
/// <param name="connInfo">connection info</param>
|
||||
/// <param name="databaseExists">flag indicating whether to create taskhelper for existing database or not</param>
|
||||
internal static CDataContainer CreateDataContainer(
|
||||
SqlConnectionInfoWithConnection connectionInfoWithConnection,
|
||||
XmlDocument containerDoc)
|
||||
{
|
||||
CDataContainer dataContainer = new CDataContainer(ServerType.SQL, connectionInfoWithConnection, true);
|
||||
dataContainer.Init(containerDoc);
|
||||
|
||||
return dataContainer;
|
||||
}
|
||||
|
||||
internal static System.Security.SecureString BuildSecureStringFromPassword(string password) {
|
||||
internal static System.Security.SecureString BuildSecureStringFromPassword(string password)
|
||||
{
|
||||
var passwordSecureString = new System.Security.SecureString();
|
||||
if (password != null) {
|
||||
foreach (char c in password) {
|
||||
if (password != null)
|
||||
{
|
||||
foreach (char c in password)
|
||||
{
|
||||
passwordSecureString.AppendChar(c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,900 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.SqlServer.Management.Common;
|
||||
using Microsoft.SqlServer.Management.Sdk.Sfc;
|
||||
using Microsoft.SqlTools.ServiceLayer.Utility;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
{
|
||||
|
||||
|
||||
public class ActionContext
|
||||
{
|
||||
#region private members
|
||||
/// <summary>
|
||||
/// Name of the object
|
||||
/// </summary>
|
||||
string name;
|
||||
/// <summary>
|
||||
/// connection to the server
|
||||
/// </summary>
|
||||
private ServerConnection connection;
|
||||
/// <summary>
|
||||
/// Connection context
|
||||
/// </summary>
|
||||
private string contextUrn;
|
||||
/// <summary>
|
||||
/// Parent node in the tree
|
||||
/// </summary>
|
||||
//private INodeInformation parent;
|
||||
/// <summary>
|
||||
/// Weak reference to the tree node this is paired with
|
||||
/// </summary>
|
||||
WeakReference NavigableItemReference;
|
||||
/// <summary>
|
||||
/// Property handlers
|
||||
/// </summary>
|
||||
//private IList<IPropertyHandler> propertyHandlers;
|
||||
/// <summary>
|
||||
/// Property bag
|
||||
/// </summary>
|
||||
NameObjectCollection properties;
|
||||
/// <summary>
|
||||
/// Object to lock on when we are modifying public state
|
||||
/// </summary>
|
||||
private object itemStateLock = new object();
|
||||
/// <summary>
|
||||
/// Cached UrnPath
|
||||
/// </summary>
|
||||
private string urnPath;
|
||||
#endregion
|
||||
|
||||
#region constructors
|
||||
|
||||
public ActionContext(ServerConnection connection, string name, string contextUrn)
|
||||
{
|
||||
if (connection == null)
|
||||
{
|
||||
throw new ArgumentNullException("connection");
|
||||
}
|
||||
if (contextUrn == null)
|
||||
{
|
||||
throw new ArgumentNullException("context");
|
||||
}
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException("name");
|
||||
}
|
||||
this.connection = connection;
|
||||
this.contextUrn = contextUrn;
|
||||
this.name = name;
|
||||
|
||||
properties = new NameObjectCollection();
|
||||
//propertyHandlers = null;
|
||||
NavigableItemReference = null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region INodeInformation implementation
|
||||
public ServerConnection Connection
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.connection;
|
||||
}
|
||||
set
|
||||
{
|
||||
lock (this.itemStateLock)
|
||||
{
|
||||
this.connection = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
public string ContextUrn
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.contextUrn;
|
||||
}
|
||||
set
|
||||
{
|
||||
lock (this.itemStateLock)
|
||||
{
|
||||
this.contextUrn = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string NavigationContext
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetNavigationContext(this);
|
||||
}
|
||||
}
|
||||
|
||||
public string UrnPath
|
||||
{
|
||||
get
|
||||
{
|
||||
this.urnPath ??= ActionContext.BuildUrnPath(this.NavigationContext);
|
||||
return this.urnPath;
|
||||
}
|
||||
}
|
||||
|
||||
public string InvariantName
|
||||
{
|
||||
get
|
||||
{
|
||||
string name = this["UniqueName"] as string;
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
return name;
|
||||
|
||||
StringBuilder uniqueName = new StringBuilder();
|
||||
|
||||
foreach (string urnValue in GetUrnPropertyValues())
|
||||
{
|
||||
if (uniqueName.Length > 0)
|
||||
uniqueName.Append(".");
|
||||
|
||||
uniqueName.Append(urnValue);
|
||||
}
|
||||
|
||||
return (uniqueName.Length > 0) ? uniqueName.ToString() : new Urn(ContextUrn).Type;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// property bag for this node
|
||||
/// </summary>
|
||||
public object this[string name] => properties[name];
|
||||
|
||||
public object CreateObjectInstance()
|
||||
{
|
||||
return CreateObjectInstance(this.ContextUrn, this.Connection);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISfcPropertyProvider implementation
|
||||
|
||||
public NameObjectCollection GetPropertySet()
|
||||
{
|
||||
return this.properties;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NodeName helper
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.name;
|
||||
}
|
||||
set
|
||||
{
|
||||
lock (this.itemStateLock)
|
||||
{
|
||||
this.name = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region property bag support
|
||||
|
||||
public NameObjectCollection Properties
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.properties;
|
||||
}
|
||||
set
|
||||
{
|
||||
lock (this.itemStateLock)
|
||||
{
|
||||
this.properties = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region helpers
|
||||
|
||||
public static string GetNavigationContext(ActionContext source)
|
||||
{
|
||||
string context = source.ContextUrn;
|
||||
// see if this is a folder
|
||||
string name = source["UniqueName"] as string;
|
||||
if (name == null || name.Length == 0)
|
||||
{
|
||||
name = source.Name;
|
||||
}
|
||||
string queryHint = source["QueryHint"] as string;
|
||||
if (queryHint == null || queryHint.Length == 0)
|
||||
{
|
||||
context = string.Format(
|
||||
System.Globalization.CultureInfo.InvariantCulture
|
||||
, "{0}/Folder[@Name='{1}']"
|
||||
, source.ContextUrn
|
||||
, Urn.EscapeString(name));
|
||||
}
|
||||
else
|
||||
{
|
||||
context = string.Format(
|
||||
System.Globalization.CultureInfo.InvariantCulture
|
||||
, "{0}/Folder[@Name='{1}' and @Type='{2}']"
|
||||
, source.ContextUrn
|
||||
, Urn.EscapeString(name)
|
||||
, Urn.EscapeString(queryHint));
|
||||
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the values of the keys in the current objects Urn
|
||||
/// e.g. For Table[@Name='Foo' and @Schema='Bar'] return Foo and Bar
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private IEnumerable<string> GetUrnPropertyValues()
|
||||
{
|
||||
Urn urn = new Urn(ContextUrn);
|
||||
Enumerator enumerator = new Enumerator();
|
||||
RequestObjectInfo request = new RequestObjectInfo(urn, RequestObjectInfo.Flags.UrnProperties);
|
||||
|
||||
ObjectInfo info = enumerator.Process(connection, request);
|
||||
|
||||
if (info == null || info.UrnProperties == null)
|
||||
yield break;
|
||||
|
||||
// Special order for Schema and Name
|
||||
if (properties.Contains("Schema"))
|
||||
yield return urn.GetAttribute("Schema");
|
||||
|
||||
if (properties.Contains("Name"))
|
||||
yield return urn.GetAttribute("Name");
|
||||
|
||||
foreach (ObjectProperty obj in info.UrnProperties)
|
||||
{
|
||||
if (obj.Name.Equals("Name", StringComparison.OrdinalIgnoreCase) || obj.Name.Equals("Schema", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
yield return urn.GetAttribute(obj.Name);
|
||||
}
|
||||
}
|
||||
|
||||
public static string BuildUrnPath(string urn)
|
||||
{
|
||||
StringBuilder urnPathBuilder = new StringBuilder(urn != null ? urn.Length : 0);
|
||||
|
||||
string folderName = string.Empty;
|
||||
bool replaceLeafValueInQuery = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(urn))
|
||||
{
|
||||
Urn urnObject = new Urn(urn);
|
||||
|
||||
while (urnObject != null)
|
||||
{
|
||||
string objectType = urnObject.Type;
|
||||
|
||||
if (string.CompareOrdinal(objectType, "Folder") == 0)
|
||||
{
|
||||
folderName = urnObject.GetAttribute("Name").Replace(" ", "");
|
||||
if (folderName != null)
|
||||
{
|
||||
objectType = string.Format("{0}Folder", folderName);
|
||||
}
|
||||
}
|
||||
|
||||
// Build the path
|
||||
if (urnPathBuilder.Length > 0)
|
||||
{
|
||||
urnPathBuilder.Insert(0, '/');
|
||||
}
|
||||
|
||||
if (objectType.Length > 0)
|
||||
{
|
||||
urnPathBuilder.Insert(0, objectType);
|
||||
}
|
||||
|
||||
// Remove one element from the urn
|
||||
urnObject = urnObject.Parent;
|
||||
}
|
||||
|
||||
// Build the query
|
||||
if (replaceLeafValueInQuery)
|
||||
{
|
||||
// This is another special case for DTS urns.
|
||||
// When we want to request data for an individual package
|
||||
// we need to use a special urn with Leaf="2" attribute,
|
||||
// replacing the Leaf='1' that comes from OE.
|
||||
urnObject = new Urn(urn.Replace("@Leaf='1'", "@Leaf='2'"));
|
||||
}
|
||||
else
|
||||
{
|
||||
urnObject = new Urn(urn);
|
||||
}
|
||||
}
|
||||
|
||||
return urnPathBuilder.ToString();
|
||||
}
|
||||
|
||||
public static object CreateObjectInstance(string urn, ServerConnection serverConnection)
|
||||
{
|
||||
if (string.IsNullOrEmpty(urn))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
SfcObjectQuery oq = null;
|
||||
Urn urnObject = new Microsoft.SqlServer.Management.Sdk.Sfc.Urn(urn);
|
||||
|
||||
// i have to find domain from Urn.
|
||||
// DomainInstanceName thrown NotImplemented Exception
|
||||
// so, i have to walk Urn tree to the top
|
||||
Urn current = urnObject;
|
||||
while (current.Parent != null)
|
||||
{
|
||||
current = current.Parent;
|
||||
}
|
||||
string domainName = current.Type;
|
||||
|
||||
if (domainName == "Server")
|
||||
{
|
||||
oq = new SfcObjectQuery(new Microsoft.SqlServer.Management.Smo.Server(serverConnection));
|
||||
}
|
||||
else
|
||||
{
|
||||
SqlConnection connection = serverConnection.SqlConnectionObject;
|
||||
if (connection == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// no need to check return value - this method will throw, if domain is incorrect
|
||||
SfcDomainInfo ddi = Microsoft.SqlServer.Management.Sdk.Sfc.SfcRegistration.Domains[domainName];
|
||||
|
||||
ISfcDomain domain = (ISfcDomain)Activator.CreateInstance(ddi.RootType, new SqlStoreConnection(connection));
|
||||
|
||||
oq = new SfcObjectQuery(domain);
|
||||
}
|
||||
|
||||
foreach (object obj in oq.ExecuteIterator(new SfcQueryExpression(urn), null, null))
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Trace.TraceError(ex.Message);
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class DataContainerXmlGenerator
|
||||
{
|
||||
#region private members
|
||||
/// <summary>
|
||||
/// additional xml to be passed to the dialog
|
||||
/// </summary>
|
||||
protected string rawXml = string.Empty;
|
||||
/// <summary>
|
||||
/// do not pass this type information to the dialog.
|
||||
/// e.g. New Database menu item on an existing database should not pass the database name through,
|
||||
/// so we set itemType as Database.
|
||||
/// </summary>
|
||||
protected string? itemType = string.Empty;
|
||||
/// <summary>
|
||||
/// Additional query to perform and pass the results to the dialog.
|
||||
/// </summary>
|
||||
protected string? invokeMultiChildQueryXPath = null;
|
||||
|
||||
private ActionContext context;
|
||||
/// <summary>
|
||||
/// The node in the hierarchy that owns this
|
||||
/// </summary>
|
||||
public virtual ActionContext Context
|
||||
{
|
||||
get { return context; }
|
||||
set { context = value; }
|
||||
}
|
||||
|
||||
private string mode;
|
||||
/// <summary>
|
||||
/// mode
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// "new" "properties"
|
||||
/// </example>
|
||||
public string Mode
|
||||
{
|
||||
get { return mode; }
|
||||
set { mode = value; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region construction
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public DataContainerXmlGenerator(ActionContext context, string mode = "new")
|
||||
{
|
||||
this.context = context;
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IObjectBuilder implementation
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddProperty(string name, object value)
|
||||
{
|
||||
// RAWXML is xml that is added to the document we're passing to the dialog with no additional
|
||||
// processing
|
||||
if (string.Compare(name, "rawxml", StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
this.rawXml += value.ToString();
|
||||
}
|
||||
// ITEMTYPE is for new menu items where we do not want to pass in the information for this type
|
||||
// e.g. New Database menu item on an existing database should not pass the database name through,
|
||||
// so we set ITEMTYPE as Database.
|
||||
else if (string.Compare(name, "itemtype", StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
this.itemType = value.ToString();
|
||||
}
|
||||
// Allows us to query below the current level in the enumerator and pass the results through to
|
||||
// the dialog. Usefull for Do xyz on all for menu's on folders.
|
||||
else if (string.Compare(name, "multichildqueryxpath", StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
this.invokeMultiChildQueryXPath = value.ToString();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region xml
|
||||
#region xml document generation
|
||||
/// <summary>
|
||||
/// Generate an XmlDocument that contains all of the context needed to launch a dialog
|
||||
/// </summary>
|
||||
/// <returns>XmlDocument</returns>
|
||||
public virtual XmlDocument GenerateXmlDocument()
|
||||
{
|
||||
MemoryStream memoryStream = new MemoryStream();
|
||||
// build the xml
|
||||
XmlTextWriter xmlWriter = new XmlTextWriter(memoryStream, Encoding.UTF8);
|
||||
|
||||
// write out the document headers
|
||||
StartXmlDocument(xmlWriter);
|
||||
// write xml specific to each connection type
|
||||
GenerateConnectionXml(xmlWriter);
|
||||
// generate the xml specific to the item we are being launched against
|
||||
GenerateItemContext(xmlWriter);
|
||||
// write out any of out properties to the document
|
||||
WritePropertiesToXml(xmlWriter);
|
||||
// close the document headers
|
||||
EndXmlDocument(xmlWriter);
|
||||
|
||||
// make sure everything is commited
|
||||
xmlWriter.Flush();
|
||||
|
||||
// Resets the stream to the beginning
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
// done composing the XML string, now build the document
|
||||
XmlDocument doc = new XmlDocument();
|
||||
|
||||
// don't lose leading or trailing whitespace
|
||||
doc.PreserveWhitespace = true;
|
||||
|
||||
// directly create the document from the memoryStream.
|
||||
// We do this because using an xmlreader in between would an extra
|
||||
// overhead and it also messes up the new line characters in the original
|
||||
// stream (converts all \r to \n).-anchals
|
||||
doc.Load(memoryStream);
|
||||
|
||||
return doc;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region document start/end
|
||||
/// <summary>
|
||||
/// Write the starting elements needed by the dialog framework
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">XmlWriter that these elements will be written to</param>
|
||||
protected virtual void StartXmlDocument(XmlWriter xmlWriter)
|
||||
{
|
||||
XmlGeneratorHelper.StartXmlDocument(xmlWriter);
|
||||
}
|
||||
/// <summary>
|
||||
/// Close the elements needed by the dialog framework
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">XmlWriter that these elements will be written to</param>
|
||||
protected virtual void EndXmlDocument(XmlWriter xmlWriter)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null.");
|
||||
|
||||
// close params
|
||||
xmlWriter.WriteEndElement();
|
||||
// close formdescription
|
||||
xmlWriter.WriteEndElement();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region server specific generation
|
||||
/// <summary>
|
||||
/// Generate the XML that will allow the dialog to connect to the server
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">XmlWriter that these elements will be written to</param>
|
||||
protected virtual void GenerateConnectionXml(XmlWriter xmlWriter)
|
||||
{
|
||||
XmlGeneratorHelper.GenerateConnectionXml(xmlWriter, this.Context);
|
||||
}
|
||||
/// <summary>
|
||||
/// Generate SQL Server specific connection information
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">XmlWriter that these elements will be written to</param>
|
||||
protected virtual void GenerateSqlConnectionXml(XmlWriter xmlWriter)
|
||||
{
|
||||
XmlGeneratorHelper.GenerateSqlConnectionXml(xmlWriter, this.Context);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region item context generation
|
||||
/// <summary>
|
||||
/// Generate context specific to the node this menu item is being launched against.
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">XmlWriter that these elements will be written to</param>
|
||||
protected virtual void GenerateItemContext(XmlWriter xmlWriter)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null.");
|
||||
|
||||
// There are two ways we can add context information.
|
||||
// The first is just off of the node we were launched against. We will break the urn down
|
||||
// into it's individual components. And pass them to the dialog.
|
||||
// The second is by performing a query relative to the node we were launched against
|
||||
// and adding any urns that are returned. No other process will be performed on the urn
|
||||
|
||||
// see if we are invoking on single, or multiple items
|
||||
if (InvokeOnSingleItemOnly())
|
||||
{
|
||||
// no query, just an individual item
|
||||
GenerateIndividualItemContext(xmlWriter);
|
||||
}
|
||||
else
|
||||
{
|
||||
GenerateMultiItemContext(xmlWriter);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Generate the context for an individual item.
|
||||
/// While Generating the context we will break down the Urn to it's individual elements
|
||||
/// and pass each Type attribute in individually.
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">XmlWriter that these elements will be written to</param>
|
||||
protected virtual void GenerateIndividualItemContext(XmlWriter xmlWriter)
|
||||
{
|
||||
XmlGeneratorHelper.GenerateIndividualItemContext(xmlWriter, itemType, this.Context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate Context for multiple items.
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">XmlWriter that these elements will be written to</param>
|
||||
protected virtual void GenerateMultiItemContext(XmlWriter xmlWriter)
|
||||
{
|
||||
// there will be a query performed
|
||||
GenerateItemContextFromQuery(xmlWriter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate Context with the results of a Query. We will just pass in the multiple
|
||||
/// Urn's if any that are the results of the query.
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">XmlWriter that these elements will be written to</param>
|
||||
protected virtual void GenerateItemContextFromQuery(XmlWriter xmlWriter)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null.");
|
||||
|
||||
// generate the request
|
||||
Request request = new Request();
|
||||
// only need urn
|
||||
request.Fields = new string[] { "Urn" };
|
||||
request.Urn = new Urn(this.Context.ContextUrn + "/" + this.invokeMultiChildQueryXPath);
|
||||
|
||||
DataTable dt;
|
||||
|
||||
// run the query
|
||||
Enumerator enumerator = new Enumerator();
|
||||
EnumResult result = enumerator.Process(this.Context.Connection, request);
|
||||
|
||||
if (result.Type == ResultType.DataTable)
|
||||
{
|
||||
dt = result;
|
||||
}
|
||||
else
|
||||
{
|
||||
dt = ((DataSet)result).Tables[0];
|
||||
}
|
||||
|
||||
//TODO: Consider throwing if there are no results.
|
||||
// Write the results to the XML document
|
||||
foreach (DataRow row in dt.Rows)
|
||||
{
|
||||
WriteUrnInformation(xmlWriter, row[0].ToString());
|
||||
}
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// Writes a Urn to the XML. If this is an Olap connection we will also write out
|
||||
/// the Olap Path, which is the AMO equivelent of a Urn.
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">XmlWriter that these elements will be written to</param>
|
||||
/// <param name="urn">Urn to be written</param>
|
||||
protected virtual void WriteUrnInformation(XmlWriter xmlWriter, string? urn)
|
||||
{
|
||||
XmlGeneratorHelper.WriteUrnInformation(xmlWriter, urn, this.Context);
|
||||
}
|
||||
/// <summary>
|
||||
/// Get the list of Urn attributes for this item.
|
||||
/// </summary>
|
||||
/// <param name="urn">Urn to be checked</param>
|
||||
/// <returns>string array of Urn attribute names. This can be zero length but will not be null</returns>
|
||||
protected virtual string[] GetUrnAttributes(Urn urn)
|
||||
{
|
||||
string[]? urnAttributes = null;
|
||||
|
||||
if (urn.XPathExpression != null && urn.XPathExpression.Length > 0)
|
||||
{
|
||||
int index = urn.XPathExpression.Length - 1;
|
||||
if (index > 0)
|
||||
{
|
||||
System.Collections.SortedList list = urn.XPathExpression[index].FixedProperties;
|
||||
System.Collections.ICollection keys = list.Keys;
|
||||
|
||||
urnAttributes = new string[keys.Count];
|
||||
|
||||
int i = 0;
|
||||
foreach (object o in keys)
|
||||
{
|
||||
string? key = o.ToString();
|
||||
if (key != null)
|
||||
{
|
||||
urnAttributes[i++] = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return urnAttributes != null ? urnAttributes : new string[0];
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region write properties
|
||||
/// <summary>
|
||||
/// Write properties set for this menu item. These can be set to pass different information
|
||||
/// to the dialog independently of the node type.
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">XmlWriter that these elements will be written to</param>
|
||||
protected virtual void WritePropertiesToXml(XmlWriter xmlWriter)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null.");
|
||||
|
||||
// mode could indicate properties or new
|
||||
if (Mode != null && Mode.Length > 0)
|
||||
{
|
||||
xmlWriter.WriteElementString("mode", Mode);
|
||||
}
|
||||
// raw xml to be passed to the dialog.
|
||||
// mostly used to control instance awareness.
|
||||
if (rawXml != null && rawXml.Length > 0)
|
||||
{
|
||||
xmlWriter.WriteRaw(rawXml);
|
||||
}
|
||||
// mostly used to restrict the context for new item off of an item of that type
|
||||
// some dialogs require this is passed in so they know what item type they are
|
||||
// supposed to be creating.
|
||||
if (this.itemType.Length > 0)
|
||||
{
|
||||
xmlWriter.WriteElementString("itemtype", this.itemType);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region protected helpers
|
||||
/// <summary>
|
||||
/// Inidicates whether the source is a single or multiple items.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual bool InvokeOnSingleItemOnly()
|
||||
{
|
||||
return (this.invokeMultiChildQueryXPath == null || this.invokeMultiChildQueryXPath.Length == 0);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// provides helper methods to generate LaunchForm XML and launch certain wizards and dialogs
|
||||
/// </summary>
|
||||
public static class XmlGeneratorHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Write the starting elements needed by the dialog framework
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">XmlWriter that these elements will be written to</param>
|
||||
public static void StartXmlDocument(XmlWriter xmlWriter)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null.");
|
||||
|
||||
xmlWriter.WriteStartElement("formdescription");
|
||||
xmlWriter.WriteStartElement("params");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a Urn to the XML. If this is an Olap connection we will also write out
|
||||
/// the Olap Path, which is the AMO equivelent of a Urn.
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">XmlWriter that these elements will be written to</param>
|
||||
/// <param name="urn">Urn to be written</param>
|
||||
public static void WriteUrnInformation(XmlWriter xmlWriter, string urn, ActionContext context)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null.");
|
||||
|
||||
// write the Urn
|
||||
xmlWriter.WriteElementString("urn", urn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate the XML that will allow the dialog to connect to the server
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">XmlWriter that these elements will be written to</param>
|
||||
public static void GenerateConnectionXml(XmlWriter xmlWriter, ActionContext context)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null.");
|
||||
|
||||
// framework also needs to know the type
|
||||
string serverType = string.Empty;
|
||||
|
||||
// Generate Connection specific XML.
|
||||
if (context.Connection is ServerConnection)
|
||||
{
|
||||
GenerateSqlConnectionXml(xmlWriter, context);
|
||||
serverType = "sql";
|
||||
}
|
||||
else
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(false, "Warning: Connection type is unknown.");
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.Assert(serverType.Length > 0, "serverType has not been defined");
|
||||
xmlWriter.WriteElementString("servertype", serverType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate SQL Server specific connection information
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">XmlWriter that these elements will be written to</param>
|
||||
public static void GenerateSqlConnectionXml(XmlWriter xmlWriter, ActionContext context)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null.");
|
||||
|
||||
// write the server name
|
||||
xmlWriter.WriteElementString("servername", context.Connection.ServerInstance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate the context for an individual item.
|
||||
/// While Generating the context we will break down the Urn to it's individual elements
|
||||
/// and pass each Type attribute in individually.
|
||||
/// </summary>
|
||||
/// <param name="xmlWriter">XmlWriter that these elements will be written to</param>
|
||||
public static void GenerateIndividualItemContext(XmlWriter xmlWriter, string itemType, ActionContext context)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(xmlWriter != null, "xmlWriter should never be null.");
|
||||
System.Diagnostics.Debug.Assert(context.ContextUrn != null, "No context available.");
|
||||
|
||||
Urn urn = new Urn(context.ContextUrn);
|
||||
|
||||
foreach (KeyValuePair<string, string> item in ExtractUrnPart(itemType, urn))
|
||||
{
|
||||
xmlWriter.WriteElementString(item.Key, item.Value);
|
||||
}
|
||||
|
||||
// if we are filtering out the information for this level (e.g. new database on a database should not
|
||||
// pass in the information relating to the selected database. We need to make sure that the Urn we pass
|
||||
// in is trimmed as well.
|
||||
Urn sourceUrn = new Urn(context.ContextUrn);
|
||||
if (itemType != null
|
||||
&& itemType.Length > 0
|
||||
&& sourceUrn.Type == itemType)
|
||||
{
|
||||
sourceUrn = sourceUrn.Parent;
|
||||
}
|
||||
|
||||
// as well as breaking everything down we will write the Urn directly
|
||||
// into the XML. Some dialogs will use the individual items, some will
|
||||
// use the Urn.
|
||||
WriteUrnInformation(xmlWriter, sourceUrn, context);
|
||||
}
|
||||
|
||||
public static IEnumerable<KeyValuePair<string, string>> ExtractUrnPart(string itemType, Urn urn)
|
||||
{
|
||||
// break the urn up into individual xml elements, and add each item
|
||||
// so Database[@Name='foo']/User[@Name='bar']
|
||||
// will become
|
||||
// <database>foo</database>
|
||||
// <user>bar</user>
|
||||
// Note: We don't care about server. It is taken care of elsewhere.
|
||||
// The dialogs need every item to be converted to lower case or they will not
|
||||
// be able to retrieve the information.
|
||||
do
|
||||
{
|
||||
// server information has already gone in, and is server type specific
|
||||
// don't get it from the urn
|
||||
if (urn.Parent != null)
|
||||
{
|
||||
// get the attributes for this part of the Urn. For Olap this is ID, for
|
||||
// everything else it is usually Name, although Schema may also be used for SQL
|
||||
string[] urnAttributes = UrnUtils.GetUrnAttributes(urn);
|
||||
|
||||
// make sure we are not supposed to skip this type. The skip allows us to bring up a "new"
|
||||
// dialog on an item of that type without passing in context.
|
||||
// e.g. New Database... on AdventureWorks should not pass in <database>AdventureWorks</Database>
|
||||
if (string.Compare(urn.Type, itemType, StringComparison.OrdinalIgnoreCase) != 0)
|
||||
{
|
||||
for (int i = 0; i < urnAttributes.Length; ++i)
|
||||
{
|
||||
// Some Urn attributes require special handling. Don't ask me why
|
||||
string thisUrnAttribute = urnAttributes[i].ToLower(CultureInfo.InvariantCulture);
|
||||
string elementName;
|
||||
switch (thisUrnAttribute)
|
||||
{
|
||||
case "schema":
|
||||
case "categoryid":
|
||||
elementName = thisUrnAttribute;
|
||||
break;
|
||||
default:
|
||||
elementName = urn.Type.ToLower(CultureInfo.InvariantCulture); // I think it's always the same as thisUrnAttribute but I'm not sure
|
||||
break;
|
||||
}
|
||||
yield return new KeyValuePair<string, string>(elementName, urn.GetAttribute(urnAttributes[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
urn = urn.Parent;
|
||||
}
|
||||
while (urn != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,7 +207,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
/// </returns>
|
||||
protected virtual bool DoPreProcessExecution(RunType runType, out ExecutionMode executionResult)
|
||||
{
|
||||
//ask the framework to do normal execution by calling OnRunNOw methods
|
||||
//ask the framework to do normal execution by calling OnRunNow methods
|
||||
//of the views one by one
|
||||
executionResult = ExecutionMode.Success;
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// 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.IO;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
|
||||
using Microsoft.SqlServer.Management.Sdk.Sfc;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides helper functions for converting urn enumerator urns
|
||||
/// to olap path equivilent.
|
||||
/// </summary>
|
||||
#if DEBUG || EXPOSE_MANAGED_INTERNALS
|
||||
public
|
||||
#else
|
||||
internal
|
||||
#endif
|
||||
class UrnDataPathConverter
|
||||
{
|
||||
// static only members
|
||||
private UrnDataPathConverter()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a Urn to and olap compatible datapath string
|
||||
/// </summary>
|
||||
/// <param name="urn">Source urn</param>
|
||||
/// <returns>string that Xml that can be used as an olap path</returns>
|
||||
/// <remarks>
|
||||
/// Node types are
|
||||
/// ServerID
|
||||
/// DatabaseID
|
||||
/// CubeID
|
||||
/// DimensionID
|
||||
/// MeasureGroupID
|
||||
/// PartitionID
|
||||
/// MiningStructureID
|
||||
/// MininingModelID
|
||||
///
|
||||
/// These currently map mostly to enuerator types with the addition of ID
|
||||
///
|
||||
/// string is of the format <ObjectTypeID>ObjectID</ObjectTypeID><3E>.<ObjectTypeID>ObjectID</ObjectTypeID>
|
||||
///
|
||||
/// </remarks>
|
||||
public static string ConvertUrnToDataPath(Urn urn)
|
||||
{
|
||||
String element = String.Empty;
|
||||
if(urn == null)
|
||||
{
|
||||
throw new ArgumentNullException("urn");
|
||||
}
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
XmlTextWriter xmlWriter = new XmlTextWriter(stringWriter);
|
||||
|
||||
ConvertUrnToDataPath(urn, xmlWriter);
|
||||
|
||||
xmlWriter.Flush();
|
||||
xmlWriter.Close();
|
||||
|
||||
return stringWriter.ToString();
|
||||
}
|
||||
/// <summary>
|
||||
/// Datapath conversion helper. Does the conversion using XmlWriter and recursion.
|
||||
/// </summary>
|
||||
/// <param name="urn">Urn to be converted</param>
|
||||
/// <param name="writer">XmlWriter that the results will be written to.</param>
|
||||
private static void ConvertUrnToDataPath(Urn urn, XmlWriter xmlWriter)
|
||||
{
|
||||
if(urn == null)
|
||||
{
|
||||
throw new ArgumentNullException("urn");
|
||||
}
|
||||
if(xmlWriter == null)
|
||||
{
|
||||
throw new ArgumentNullException("xmlWriter");
|
||||
}
|
||||
|
||||
// preserve the order so do the parent first
|
||||
Urn parent = urn.Parent;
|
||||
if(parent != null)
|
||||
{
|
||||
ConvertUrnToDataPath(parent, xmlWriter);
|
||||
}
|
||||
|
||||
String tag = urn.Type;
|
||||
|
||||
// don't put server into the olap path.
|
||||
if(tag != "OlapServer")
|
||||
{
|
||||
xmlWriter.WriteElementString(tag + "ID", urn.GetAttribute("ID"));
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Convert an xml body string that is compatible with a string representation
|
||||
/// (i.e. deal with < > &)
|
||||
/// </summary>
|
||||
/// <param name="s">source</param>
|
||||
/// <returns>string that can be used as the body for xml stored in a string</returns>
|
||||
public static string TokenizeXml(string source)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(false, "do not use this function. See bugs 322423 and 115450 in SQLBU Defect tracking");
|
||||
|
||||
if(null == source) return String.Empty;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach(char c in source)
|
||||
{
|
||||
switch(c)
|
||||
{
|
||||
case '<':
|
||||
sb.Append("<");
|
||||
break;
|
||||
case '>':
|
||||
sb.Append(">");
|
||||
break;
|
||||
case '&':
|
||||
sb.Append("&");
|
||||
break;
|
||||
default:
|
||||
sb.Append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// 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 Microsoft.SqlServer.Management.Sdk.Sfc;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Management
|
||||
{
|
||||
internal class UrnUtils
|
||||
{
|
||||
private UrnUtils () { }
|
||||
|
||||
/// <summary>
|
||||
/// Get the list of Urn attributes for this item.
|
||||
/// </summary>
|
||||
/// <param name="urn">Urn to be checked</param>
|
||||
/// <returns>String array of Urn attribute names. This can be zero length but will not be null</returns>
|
||||
public static string[] GetUrnAttributes(Urn urn)
|
||||
{
|
||||
String[]? urnAttributes = null;
|
||||
|
||||
if(urn.XPathExpression != null && urn.XPathExpression.Length > 0)
|
||||
{
|
||||
int index = urn.XPathExpression.Length - 1;
|
||||
if(index >= 0)
|
||||
{
|
||||
System.Collections.SortedList list = urn.XPathExpression[index].FixedProperties;
|
||||
System.Collections.ICollection keys = list.Keys;
|
||||
|
||||
urnAttributes = new String[keys.Count];
|
||||
|
||||
int i = 0;
|
||||
foreach(object o in keys)
|
||||
{
|
||||
string? key = o.ToString();
|
||||
if (key != null)
|
||||
urnAttributes[i++] = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
return urnAttributes != null ? urnAttributes : new String[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user