Add AQL Assessment service (#946)

[SQL Assessment API](https://docs.microsoft.com/en-us/sql/sql-assessment-api/sql-assessment-api-overview) provides a mechanism to evaluate the configuration
of SQL Server for best practices. SQL Assessment API gives a list
of recommended actions to improve SQL Server performance or security.

The SQL Assessment service is used by the expected SQL Assessment
feature of Azure Data Studio. 

SqlAssessmentService forwards JSONRPC calls to SQL Assessment engine
and wraps results as a response.

`assessment/getAssessmentItems` returns a set of checks 
applicable to a given target.

`assessment/invoke` returns a set of recommendations
for improving SQL Server instance or database configurations. 

`assessment/generateScript` returns a T-SQL script for storing
an assessment result set to a SQL data table.
This commit is contained in:
Aleksei Guzev
2020-04-24 10:52:55 +03:00
committed by GitHub
parent 89699823bf
commit bcc1f2a486
15 changed files with 1439 additions and 5 deletions

View File

@@ -0,0 +1,129 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Generic;
using Microsoft.SqlServer.Management.Assessment;
namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts
{
/// <summary>
/// Parameters for executing a query from a provided string
/// </summary>
public class AssessmentParams
{
/// <summary>
/// Gets or sets the owner uri to get connection from
/// </summary>
public string OwnerUri { get; set; }
/// <summary>
/// Gets or sets the target type
/// </summary>
public SqlObjectType TargetType { get; set; }
}
/// <summary>
/// Describes an item returned by SQL Assessment RPC methods
/// </summary>
public class AssessmentItemInfo
{
/// <summary>
/// Gets or sets assessment ruleset version.
/// </summary>
public string RulesetVersion { get; set; }
/// <summary>
/// Gets or sets assessment ruleset name
/// </summary>
public string RulesetName { get; set; }
/// <summary>
/// Gets or sets assessed target's type.
/// Supported values: 1 - server, 2 - database.
/// </summary>
public SqlObjectType TargetType { get; set; }
/// <summary>
/// Gets or sets the assessed object's name.
/// </summary>
public string TargetName { get; set; }
/// <summary>
/// Gets or sets check's ID.
/// </summary>
public string CheckId { get; set; }
/// <summary>
/// Gets or sets tags assigned to this item.
/// </summary>
public string[] Tags { get; set; }
/// <summary>
/// Gets or sets a display name for this item.
/// </summary>
public string DisplayName { get; set; }
/// <summary>
/// Gets or sets a brief description of the item's purpose.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Gets or sets a <see cref="string"/> containing
/// an link to a page providing detailed explanation
/// of the best practice.
/// </summary>
public string HelpLink { get; set; }
/// <summary>
/// Gets or sets a <see cref="string"/> indicating
/// severity level assigned to this items.
/// Values are: "Information", "Warning", "Critical".
/// </summary>
public string Level { get; set; }
}
/// <summary>
/// Generic SQL Assessment Result
/// </summary>
/// <typeparam name="T">
/// Result item's type derived from <see cref="AssessmentItemInfo"/>
/// </typeparam>
public class AssessmentResultData<T>
where T : AssessmentItemInfo
{
/// <summary>
/// Gets the collection of assessment results.
/// </summary>
public List<T> Items { get; } = new List<T>();
/// <summary>
/// Gets or sets SQL Assessment API version.
/// </summary>
public string ApiVersion { get; set; }
}
/// <summary>
/// Generic SQL Assessment Result
/// </summary>
/// <typeparam name="T">
/// Result item's type derived from <see cref="AssessmentItemInfo"/>
/// </typeparam>
public class AssessmentResult<T> : AssessmentResultData<T>
where T : AssessmentItemInfo
{
/// <summary>
/// Gets or sets a value indicating
/// if assessment operation was successful.
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Gets or sets an status message for the operation.
/// </summary>
public string ErrorMessage { get; set; }
}
}

View File

@@ -0,0 +1,56 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Generic;
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.TaskServices;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts
{
/// <summary>
/// Parameters for executing a query from a provided string
/// </summary>
public class GenerateScriptParams
{
/// <summary>
/// Gets or sets a list of assessment result items
/// to be written to a table
/// </summary>
public List<AssessmentResultItem> Items { get; set; }
public TaskExecutionMode TaskExecutionMode { get; set; }
public string TargetServerName { get; set; }
public string TargetDatabaseName { get; set; }
}
public class GenerateScriptResult
{
/// <summary>
/// Gets or sets a value indicating
/// if assessment operation was successful
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Gets or sets an status message for the operation
/// </summary>
public string ErrorMessage { get; set; }
/// <summary>
/// Gets or sets script text
/// </summary>
public string Script { get; set; }
}
public class GenerateScriptRequest
{
public static readonly
RequestType<GenerateScriptParams, ResultStatus> Type =
RequestType<GenerateScriptParams, ResultStatus>.Create("assessment/generateScript");
}
}

View File

@@ -0,0 +1,33 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts
{
/// <summary>
/// Parameters for executing a query from a provided string
/// </summary>
public class GetAssessmentItemsParams : AssessmentParams
{
// a placeholder for future specialization
}
/// <summary>
/// Describes a check used to assess SQL Server objects.
/// </summary>
public class CheckInfo : AssessmentItemInfo
{
// a placeholder for future specialization
}
public class GetAssessmentItemsRequest
{
public static readonly RequestType<GetAssessmentItemsParams, AssessmentResult<CheckInfo>> Type =
RequestType<GetAssessmentItemsParams, AssessmentResult<CheckInfo>>.Create(
"assessment/getAssessmentItems");
}
}

View File

@@ -0,0 +1,82 @@
//
// 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.SqlTools.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts
{
/// <summary>
/// Parameters for executing a query from a provided string
/// </summary>
public class InvokeParams : AssessmentParams
{
// a placeholder for future specialization
}
/// <summary>
/// SQL Assessment result item kind.
/// </summary>
/// <remarks>
/// SQL Assessment run is a set of checks. Every check
/// may return a result item. Normally it is a note containing
/// recommendations on improving target's configuration.
/// But some checks may fail to obtain data due to access
/// restrictions or data integrity. In this case
/// the check produces an error or a warning.
/// </remarks>
public enum AssessmentResultItemKind
{
/// <summary>
/// SQL Assessment item contains recommendation
/// </summary>
Note = 0,
/// <summary>
/// SQL Assessment item contains a warning on
/// limited assessment capabilities
/// </summary>
Warning = 1,
/// <summary>
/// SQL Assessment item contain a description of
/// error occured in the course of assessment run
/// </summary>
Error = 2
}
/// <summary>
/// Describes an assessment result item
/// containing a recommendation based on best practices.
/// </summary>
public class AssessmentResultItem : AssessmentItemInfo
{
/// <summary>
/// Gets or sets a message to the user
/// containing the recommendation.
/// </summary>
public string Message { get; set; }
/// <summary>
/// Gets or sets result type:
/// 0 - real result, 1 - warning, 2 - error.
/// </summary>
public AssessmentResultItemKind Kind { get; set; }
/// <summary>
/// Gets or sets date and time
/// when the item had been acquired.
/// </summary>
public DateTimeOffset Timestamp { get; set; }
}
public class InvokeRequest
{
public static readonly
RequestType<InvokeParams, AssessmentResult<AssessmentResultItem>> Type =
RequestType<InvokeParams, AssessmentResult<AssessmentResultItem>>.Create("assessment/invoke");
}
}

View File

@@ -0,0 +1,151 @@
//
// 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.Diagnostics;
using System.Globalization;
using System.Text;
using System.Threading;
using Microsoft.SqlTools.ServiceLayer.Management;
using Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts;
using Microsoft.SqlTools.ServiceLayer.TaskServices;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment
{
/// <summary>
/// Generates a script storing SQL Assessment results to a table.
/// </summary>
internal sealed class GenerateScriptOperation : ITaskOperation, IDisposable
{
private readonly CancellationTokenSource cancellation = new CancellationTokenSource();
private bool disposed = false;
/// <summary>
/// Gets the unique id associated with this instance.
/// </summary>
public string OperationId { get; set; }
/// <summary>
/// Gets the parameters containing assessment results
/// to be stored in a data table.
/// </summary>
public GenerateScriptParams Parameters { get; }
/// <summary>
/// Gets or sets the error message text
/// if an error occurred during task execution
/// </summary>
public string ErrorMessage { get; set; }
/// <summary>
/// Gets or sets the sql task that's executing the operation
/// </summary>
public SqlTask SqlTask { get; set; }
public GenerateScriptOperation(GenerateScriptParams parameters)
{
Validate.IsNotNull(nameof(parameters), parameters);
Parameters = parameters;
}
/// <summary>
/// Execute a task
/// </summary>
/// <param name="mode">Task execution mode (e.g. script or execute)</param>
/// <exception cref="InvalidOperationException">
/// The method has been called twice in parallel for the same instance.
/// </exception>
public void Execute(TaskExecutionMode mode)
{
try
{
var scriptText = GenerateScript(Parameters, cancellation.Token);
if (scriptText != null)
{
SqlTask?.AddScript(SqlTaskStatus.Succeeded, scriptText);
}
}
catch (Exception e)
{
ErrorMessage = e.Message;
Logger.Write(TraceEventType.Error, string.Format(
CultureInfo.InvariantCulture,
"SQL Assessment: generate script operation failed with exception {0}",
e.Message));
throw;
}
}
public void Cancel()
{
cancellation.Cancel();
}
#region Helpers
internal static string GenerateScript(GenerateScriptParams generateScriptParams,
CancellationToken cancellationToken)
{
const string scriptPrologue =
@"IF (NOT EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'AssessmentResult'))
BEGIN
CREATE TABLE [dbo].[AssessmentResult](
[CheckName] [nvarchar](max) NOT NULL,
[CheckId] [nvarchar](max) NOT NULL,
[RulesetName] [nvarchar](max) NOT NULL,
[RulesetVersion] [nvarchar](max) NOT NULL,
[Severity] [nvarchar](max) NOT NULL,
[Message] [nvarchar](max) NOT NULL,
[TargetPath] [nvarchar](max) NOT NULL,
[TargetType] [nvarchar](max) NOT NULL,
[HelpLink] [nvarchar](max) NOT NULL,
[Timestamp] [datetimeoffset](7) NOT NULL
)
END
GO
INSERT INTO [dbo].[AssessmentResult] ([CheckName],[CheckId],[RulesetName],[RulesetVersion],[Severity],[Message],[TargetPath],[TargetType],[HelpLink],[Timestamp])
VALUES";
var sb = new StringBuilder();
if (generateScriptParams.Items != null)
{
sb.Append(scriptPrologue);
foreach (var item in generateScriptParams.Items)
{
cancellationToken.ThrowIfCancellationRequested();
if (item.Kind == AssessmentResultItemKind.Note)
{
sb.Append(
$"\r\n('{CUtils.EscapeStringSQuote(item.DisplayName)}','{CUtils.EscapeStringSQuote(item.CheckId)}','{CUtils.EscapeStringSQuote(item.RulesetName)}','{item.RulesetVersion}','{item.Level}','{CUtils.EscapeStringSQuote(item.Message)}','{CUtils.EscapeStringSQuote(item.TargetName)}','{item.TargetType}','{CUtils.EscapeStringSQuote(item.HelpLink)}','{item.Timestamp:yyyy-MM-dd hh:mm:ss.fff zzz}'),");
}
}
sb.Length -= 1;
}
return sb.ToString();
}
#endregion
#region IDisposable
public void Dispose()
{
if (!disposed)
{
Cancel();
cancellation.Dispose();
disposed = true;
}
}
#endregion
}
}

View File

@@ -0,0 +1,503 @@
//
// 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.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SqlServer.Management.Assessment;
using Microsoft.SqlServer.Management.Assessment.Configuration;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.TaskServices;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.ServiceLayer.Workspace;
using Microsoft.SqlTools.Utility;
using AssessmentResultItem = Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts.AssessmentResultItem;
using ConnectionType = Microsoft.SqlTools.ServiceLayer.Connection.ConnectionType;
using InvokeParams = Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts.InvokeParams;
using InvokeRequest = Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts.InvokeRequest;
namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment
{
/// <summary>
/// Service for running SQL Assessment.
/// </summary>
public sealed class SqlAssessmentService : IDisposable
{
private const string ApiVersion = "1.0";
#region Singleton Instance Implementation
private static readonly Lazy<SqlAssessmentService> LazyInstance
= new Lazy<SqlAssessmentService>(() => new SqlAssessmentService());
internal SqlAssessmentService(
ConnectionService connService,
WorkspaceService<SqlToolsSettings> workspaceService)
{
ConnectionService = connService;
WorkspaceService = workspaceService;
}
private SqlAssessmentService()
{
ConnectionService = ConnectionService.Instance;
WorkspaceService = WorkspaceService<SqlToolsSettings>.Instance;
}
/// <summary>
/// Singleton instance of the query execution service
/// </summary>
public static SqlAssessmentService Instance => LazyInstance.Value;
#endregion
#region Properties
/// <summary>
/// Gets the <see cref="Engine"/> used to run assessment operations.
/// </summary>
private Engine Engine { get; } = new Engine();
/// <summary>
/// Gets the instance of the connection service,
/// used to get the connection info for a given owner URI.
/// </summary>
private ConnectionService ConnectionService { get; }
private WorkspaceService<SqlToolsSettings> WorkspaceService { get; }
/// <summary>
/// Holds a map from the <see cref="Guid"/>
/// to a <see cref="Task"/> that is being ran.
/// </summary>
private readonly Lazy<ConcurrentDictionary<string, Task>> activeRequests =
new Lazy<ConcurrentDictionary<string, Task>>(() => new ConcurrentDictionary<string, Task>());
/// <summary>
/// Gets a map from the <see cref="Guid"/>
/// to a <see cref="Task"/> that is being ran.
/// </summary>
internal ConcurrentDictionary<string, Task> ActiveRequests => activeRequests.Value;
#endregion
/// <summary>
/// Initializes the service with the service host,
/// registers request handlers and shutdown event handler.
/// </summary>
/// <param name="serviceHost">The service host instance to register with</param>
public void InitializeService(ServiceHost serviceHost)
{
// Register handlers for requests
serviceHost.SetRequestHandler(InvokeRequest.Type, HandleInvokeRequest);
serviceHost.SetRequestHandler(GetAssessmentItemsRequest.Type, HandleGetAssessmentItemsRequest);
serviceHost.SetRequestHandler(GenerateScriptRequest.Type, HandleGenerateScriptRequest);
// Register handler for shutdown event
serviceHost.RegisterShutdownTask((shutdownParams, requestContext) =>
{
Dispose();
return Task.FromResult(0);
});
}
#region Request Handlers
internal Task HandleGetAssessmentItemsRequest(
GetAssessmentItemsParams itemsParams,
RequestContext<AssessmentResult<CheckInfo>> requestContext)
{
return this.HandleAssessmentRequest(requestContext, itemsParams, this.GetAssessmentItems);
}
internal Task HandleInvokeRequest(
InvokeParams invokeParams,
RequestContext<AssessmentResult<AssessmentResultItem>> requestContext)
{
return this.HandleAssessmentRequest(requestContext, invokeParams, this.InvokeSqlAssessment);
}
internal async Task HandleGenerateScriptRequest(
GenerateScriptParams parameters,
RequestContext<ResultStatus> requestContext)
{
GenerateScriptOperation operation = null;
try
{
operation = new GenerateScriptOperation(parameters);
TaskMetadata metadata = new TaskMetadata
{
TaskOperation = operation,
TaskExecutionMode = parameters.TaskExecutionMode,
ServerName = parameters.TargetServerName,
DatabaseName = parameters.TargetDatabaseName,
Name = SR.SqlAssessmentGenerateScriptTaskName
};
var _ = SqlTaskManager.Instance.CreateAndRun<SqlTask>(metadata);
await requestContext.SendResult(new ResultStatus
{
Success = true,
ErrorMessage = operation.ErrorMessage
});
}
catch (Exception e)
{
Logger.Write(TraceEventType.Error, "SQL Assessment: failed to generate a script. Error: " + e);
await requestContext.SendResult(new ResultStatus()
{
Success = false,
ErrorMessage = operation == null ? e.Message : operation.ErrorMessage,
});
}
}
#endregion
#region Helpers
private async Task HandleAssessmentRequest<TResult>(
RequestContext<AssessmentResult<TResult>> requestContext,
AssessmentParams requestParams,
Func<SqlObjectLocator, Task<List<TResult>>> assessmentFunction)
where TResult : AssessmentItemInfo
{
try
{
string randomUri = Guid.NewGuid().ToString();
// get connection
if (!ConnectionService.TryFindConnection(requestParams.OwnerUri, out var connInfo))
{
await requestContext.SendError(SR.SqlAssessmentQueryInvalidOwnerUri);
return;
}
ConnectParams connectParams = new ConnectParams
{
OwnerUri = randomUri,
Connection = connInfo.ConnectionDetails,
Type = ConnectionType.Default
};
if(!connInfo.TryGetConnection(ConnectionType.Default, out var connection))
{
await requestContext.SendError(SR.SqlAssessmentConnectingError);
}
var workTask = CallAssessmentEngine<TResult>(
requestParams,
connectParams,
randomUri,
assessmentFunction)
.ContinueWith(async tsk =>
{
await requestContext.SendResult(tsk.Result);
});
ActiveRequests.TryAdd(randomUri, workTask);
}
catch (Exception ex)
{
if (ex is StackOverflowException || ex is OutOfMemoryException)
{
throw;
}
await requestContext.SendError(ex.ToString());
}
}
/// <summary>
/// This function obtains a live connection, then calls
/// an assessment operation specified by <paramref name="assessmentFunc"/>
/// </summary>
/// <typeparam name="TResult">
/// SQL Assessment result item type.
/// </typeparam>
/// <param name="requestParams">
/// Request parameters passed from the host.
/// </param>
/// <param name="connectParams">
/// Connection parameters used to identify and access the target.
/// </param>
/// <param name="taskUri">
/// An URI identifying the request task to enable concurrent execution.
/// </param>
/// <param name="assessmentFunc">
/// A function performing assessment operation for given target.
/// </param>
/// <returns>
/// Returns <see cref="AssessmentResult{TResult}"/> for given target.
/// </returns>
internal async Task<AssessmentResult<TResult>> CallAssessmentEngine<TResult>(
AssessmentParams requestParams,
ConnectParams connectParams,
string taskUri,
Func<SqlObjectLocator, Task<List<TResult>>> assessmentFunc)
where TResult : AssessmentItemInfo
{
var result = new AssessmentResult<TResult>
{
ApiVersion = ApiVersion
};
await ConnectionService.Connect(connectParams);
var connection = await ConnectionService.Instance.GetOrOpenConnection(taskUri, ConnectionType.Query);
try
{
var serverInfo = ReliableConnectionHelper.GetServerVersion(connection);
var hostInfo = ReliableConnectionHelper.GetServerHostInfo(connection);
var server = new SqlObjectLocator
{
Connection = connection,
EngineEdition = GetEngineEdition(serverInfo.EngineEditionId),
Name = serverInfo.ServerName,
ServerName = serverInfo.ServerName,
Type = SqlObjectType.Server,
Urn = serverInfo.ServerName,
Version = Version.Parse(serverInfo.ServerVersion),
Platform = hostInfo.Platform
};
switch (requestParams.TargetType)
{
case SqlObjectType.Server:
Logger.Write(
TraceEventType.Verbose,
$"SQL Assessment: running an operation on a server, platform:{server.Platform}, edition:{server.EngineEdition.ToString()}, version:{server.Version}");
result.Items.AddRange(await assessmentFunc(server));
Logger.Write(
TraceEventType.Verbose,
$"SQL Assessment: finished an operation on a server, platform:{server.Platform}, edition:{server.EngineEdition.ToString()}, version:{server.Version}");
break;
case SqlObjectType.Database:
var db = GetDatabaseLocator(server, connection.Database);
Logger.Write(
TraceEventType.Verbose,
$"SQL Assessment: running an operation on a database, platform:{server.Platform}, edition:{server.EngineEdition.ToString()}, version:{server.Version}");
result.Items.AddRange(await assessmentFunc(db));
Logger.Write(
TraceEventType.Verbose,
$"SQL Assessment: finished an operation on a database, platform:{server.Platform}, edition:{server.EngineEdition.ToString()}, version:{server.Version}");
break;
}
result.Success = true;
}
finally
{
ActiveRequests.TryRemove(taskUri, out _);
ConnectionService.Disconnect(new DisconnectParams { OwnerUri = taskUri, Type = null });
}
return result;
}
/// <summary>
/// Invokes SQL Assessment and formats results.
/// </summary>
/// <param name="target">
/// A sequence of target servers or databases to be assessed.
/// </param>
/// <returns>
/// Returns a <see cref="List{AssessmentResultItem}"/>
/// containing assessment results.
/// </returns>
/// <remarks>
/// Internal for testing
/// </remarks>
internal async Task<List<AssessmentResultItem>> InvokeSqlAssessment(SqlObjectLocator target)
{
var resultsList = await Engine.GetAssessmentResultsList(target);
Logger.Write(TraceEventType.Verbose, $"SQL Assessment: got {resultsList.Count} results.");
return resultsList.Select(TranslateAssessmentResult).ToList();
}
/// <summary>
/// Gets the list of checks for given target servers or databases.
/// </summary>
/// <param name="target">
/// A sequence of target servers or databases.
/// </param>
/// <returns>
/// Returns an <see cref="IEnumerable{SqlObjectLocator}"/>
/// containing checks available for given <paramref name="target"/>.
/// </returns>
internal Task<List<CheckInfo>> GetAssessmentItems(SqlObjectLocator target)
{
var result = new List<CheckInfo>();
var resultsList = Engine.GetChecks(target).ToList();
Logger.Write(TraceEventType.Verbose, $"SQL Assessment: got {resultsList.Count} items.");
foreach (var r in resultsList)
{
var item = new CheckInfo()
{
CheckId = r.Id,
Description = r.Description,
DisplayName = r.DisplayName,
HelpLink = r.HelpLink,
Level = r.Level.ToString(),
TargetName = $"{target.ServerName}/{target.Name}",
Tags = r.Tags.ToArray(),
TargetType = target.Type,
RulesetName = Engine.Configuration.DefaultRuleset.Name,
RulesetVersion = Engine.Configuration.DefaultRuleset.Version.ToString()
};
result.Add(item);
}
return Task.FromResult(result);
}
private AssessmentResultItem TranslateAssessmentResult(IAssessmentResult r)
{
var item = new AssessmentResultItem
{
CheckId = r.Check.Id,
Description = r.Check.Description,
DisplayName = r.Check.DisplayName,
HelpLink = r.Check.HelpLink,
Level = r.Check.Level.ToString(),
Message = r.Message,
TargetName = r.TargetPath,
Tags = r.Check.Tags.ToArray(),
TargetType = r.TargetType,
RulesetVersion = Engine.Configuration.DefaultRuleset.Version.ToString(),
RulesetName = Engine.Configuration.DefaultRuleset.Name,
Timestamp = r.Timestamp
};
if (r is IAssessmentNote)
{
item.Kind = AssessmentResultItemKind.Note;
}
else if (r is IAssessmentWarning)
{
item.Kind = AssessmentResultItemKind.Warning;
}
else if (r is IAssessmentError)
{
item.Kind = AssessmentResultItemKind.Error;
}
return item;
}
/// <summary>
/// Constructs a <see cref="SqlObjectLocator"/> for specified database.
/// </summary>
/// <param name="server">Target server locator.</param>
/// <param name="databaseName">Target database name.</param>
/// <returns>Returns a locator for target database.</returns>
private static SqlObjectLocator GetDatabaseLocator(SqlObjectLocator server, string databaseName)
{
return new SqlObjectLocator
{
Connection = server.Connection,
EngineEdition = server.EngineEdition,
Name = databaseName,
Platform = server.Platform,
ServerName = server.Name,
Type = SqlObjectType.Database,
Urn = $"{server.Name}/{databaseName}",
Version = server.Version
};
}
/// <summary>
/// Converts numeric <paramref name="representation"/> of engine edition
/// returned by SERVERPROPERTY('EngineEdition').
/// </summary>
/// <param name="representation">
/// A number returned by SERVERPROPERTY('EngineEdition').
/// </param>
/// <exception cref="ArgumentOutOfRangeException">Engine edition is not supported.</exception>
/// <returns>
/// Returns a <see cref="SqlEngineEdition"/>
/// corresponding to the <paramref name="representation"/>.
/// </returns>
private static SqlEngineEdition GetEngineEdition(int representation)
{
switch (representation)
{
case 1: return SqlEngineEdition.PersonalOrDesktopEngine;
case 2: return SqlEngineEdition.Standard;
case 3: return SqlEngineEdition.Enterprise;
case 4: return SqlEngineEdition.Express;
case 5: return SqlEngineEdition.AzureDatabase;
case 6: return SqlEngineEdition.DataWarehouse;
case 7: return SqlEngineEdition.StretchDatabase;
case 8: return SqlEngineEdition.ManagedInstance;
default:
throw new ArgumentOutOfRangeException(nameof(representation),
SR.SqlAssessmentUnsuppoertedEdition(representation));
}
}
#endregion
#region IDisposable Implementation
private bool disposed;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (disposed)
{
return;
}
if (disposing)
{
foreach (var request in ActiveRequests)
{
request.Value.Dispose();
}
ActiveRequests.Clear();
}
disposed = true;
}
~SqlAssessmentService()
{
Dispose(false);
}
#endregion
}
}