From fb54566ff5ba0d21c86adb3224802bfc6dd9bbeb Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Mon, 19 Sep 2016 11:41:35 -0700 Subject: [PATCH] Add dotnet-sql-cache --- DotNetTools.sln | 7 + NuGetPackageVerifier.json | 1 + README.md | 1 + ...t.Extensions.Caching.SqlConfig.Tools.xproj | 17 ++ .../Program.cs | 157 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 11 ++ .../SqlQueries.cs | 67 ++++++++ .../project.json | 41 +++++ 8 files changed, 302 insertions(+) create mode 100644 src/Microsoft.Extensions.Caching.SqlConfig.Tools/Microsoft.Extensions.Caching.SqlConfig.Tools.xproj create mode 100644 src/Microsoft.Extensions.Caching.SqlConfig.Tools/Program.cs create mode 100644 src/Microsoft.Extensions.Caching.SqlConfig.Tools/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.Extensions.Caching.SqlConfig.Tools/SqlQueries.cs create mode 100644 src/Microsoft.Extensions.Caching.SqlConfig.Tools/project.json diff --git a/DotNetTools.sln b/DotNetTools.sln index e600c724c1..3dbe6b4f4e 100644 --- a/DotNetTools.sln +++ b/DotNetTools.sln @@ -33,6 +33,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.Secret EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.SecretManager.Tools.Tests", "test\Microsoft.Extensions.SecretManager.Tools.Tests\Microsoft.Extensions.SecretManager.Tools.Tests.xproj", "{7B331122-83B1-4F08-A119-DC846959844C}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.Caching.SqlConfig.Tools", "src\Microsoft.Extensions.Caching.SqlConfig.Tools\Microsoft.Extensions.Caching.SqlConfig.Tools.xproj", "{53F3B53D-303A-4DAA-9C38-4F55195FA5B9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -75,6 +77,10 @@ Global {7B331122-83B1-4F08-A119-DC846959844C}.Debug|Any CPU.Build.0 = Debug|Any CPU {7B331122-83B1-4F08-A119-DC846959844C}.Release|Any CPU.ActiveCfg = Release|Any CPU {7B331122-83B1-4F08-A119-DC846959844C}.Release|Any CPU.Build.0 = Release|Any CPU + {53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -90,5 +96,6 @@ Global {2F48041A-F7D1-478F-9C38-D41F0F05E8CA} = {2876B12E-5841-4792-85A8-2929AEE11885} {8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E} = {66517987-2A5A-4330-B130-207039378FD4} {7B331122-83B1-4F08-A119-DC846959844C} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134} + {53F3B53D-303A-4DAA-9C38-4F55195FA5B9} = {66517987-2A5A-4330-B130-207039378FD4} EndGlobalSection EndGlobal diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index 932af6fb14..fe8b1557a4 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -6,6 +6,7 @@ "packages": { "Microsoft.DotNet.Watcher.Tools": { }, "Microsoft.DotNet.Watcher.Core": { }, + "Microsoft.Extensions.Caching.SqlConfig.Tools": { }, "Microsoft.Extensions.SecretManager.Tools": { } } }, diff --git a/README.md b/README.md index 64eb8a2cee..e937d4d0d5 100644 --- a/README.md +++ b/README.md @@ -8,5 +8,6 @@ The project contains command-line tools for the .NET Core SDK. - [dotnet-watch](src/Microsoft.DotNet.Watcher.Tools/) - [dotnet-user-secrets](src/Microsoft.Extensions.SecretManager.Tools/) + - [dotnet-sql-cache](src/Microsoft.Extensions.Caching.SqlConfig.Tools/) This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo. diff --git a/src/Microsoft.Extensions.Caching.SqlConfig.Tools/Microsoft.Extensions.Caching.SqlConfig.Tools.xproj b/src/Microsoft.Extensions.Caching.SqlConfig.Tools/Microsoft.Extensions.Caching.SqlConfig.Tools.xproj new file mode 100644 index 0000000000..1bba570109 --- /dev/null +++ b/src/Microsoft.Extensions.Caching.SqlConfig.Tools/Microsoft.Extensions.Caching.SqlConfig.Tools.xproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 53f3b53d-303a-4daa-9c38-4f55195fa5b9 + .\obj + .\bin\ + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.Extensions.Caching.SqlConfig.Tools/Program.cs b/src/Microsoft.Extensions.Caching.SqlConfig.Tools/Program.cs new file mode 100644 index 0000000000..eb46dcb0a8 --- /dev/null +++ b/src/Microsoft.Extensions.Caching.SqlConfig.Tools/Program.cs @@ -0,0 +1,157 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Data; +using System.Data.SqlClient; +using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.Caching.SqlConfig.Tools +{ + public class Program + { + private string _connectionString = null; + private string _schemaName = null; + private string _tableName = null; + + private readonly ILogger _logger; + + public Program() + { + var loggerFactory = new LoggerFactory(); + loggerFactory.AddConsole(); + _logger = loggerFactory.CreateLogger(); + } + + public static int Main(string[] args) + { + return new Program().Run(args); + } + + public int Run(string[] args) + { + try + { + var description = "Creates table and indexes in Microsoft SQL Server database " + + "to be used for distributed caching"; + + var app = new CommandLineApplication(); + app.Name = "dotnet-sql-cache"; + app.Description = description; + + app.HelpOption("-?|-h|--help"); + + app.Command("create", command => + { + command.Description = description; + var connectionStringArg = command.Argument( + "[connectionString]", + "The connection string to connect to the database."); + var schemaNameArg = command.Argument("[schemaName]", "Name of the table schema."); + var tableNameArg = command.Argument("[tableName]", "Name of the table to be created."); + command.HelpOption("-?|-h|--help"); + + command.OnExecute(() => + { + if (string.IsNullOrEmpty(connectionStringArg.Value) + || string.IsNullOrEmpty(schemaNameArg.Value) + || string.IsNullOrEmpty(tableNameArg.Value)) + { + _logger.LogWarning("Invalid input"); + app.ShowHelp(); + return 2; + } + + _connectionString = connectionStringArg.Value; + _schemaName = schemaNameArg.Value; + _tableName = tableNameArg.Value; + + return CreateTableAndIndexes(); + }); + }); + + // Show help information if no subcommand/option was specified. + app.OnExecute(() => + { + app.ShowHelp(); + return 2; + }); + + return app.Execute(args); + } + catch (Exception exception) + { + _logger.LogCritical("An error occurred. {ErrorMessage}", exception.Message); + return 1; + } + } + + private int CreateTableAndIndexes() + { + ValidateConnectionString(); + + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + + var sqlQueries = new SqlQueries(_schemaName, _tableName); + var command = new SqlCommand(sqlQueries.TableInfo, connection); + + using (var reader = command.ExecuteReader(CommandBehavior.SingleRow)) + { + if (reader.Read()) + { + _logger.LogWarning( + $"Table with schema '{_schemaName}' and name '{_tableName}' already exists. " + + "Provide a different table name and try again."); + return 1; + } + } + + using (var transaction = connection.BeginTransaction()) + { + try + { + command = new SqlCommand(sqlQueries.CreateTable, connection, transaction); + command.ExecuteNonQuery(); + + command = new SqlCommand( + sqlQueries.CreateNonClusteredIndexOnExpirationTime, + connection, + transaction); + command.ExecuteNonQuery(); + + transaction.Commit(); + + _logger.LogInformation("Table and index were created successfully."); + } + catch (Exception ex) + { + _logger.LogError( + "An error occurred while trying to create the table and index. {ErrorMessage}", + ex.Message); + transaction.Rollback(); + + return 1; + } + } + } + + return 0; + } + + private void ValidateConnectionString() + { + try + { + new SqlConnectionStringBuilder(_connectionString); + } + catch (Exception ex) + { + throw new ArgumentException( + $"Invalid Sql server connection string '{_connectionString}'. {ex.Message}", ex); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Caching.SqlConfig.Tools/Properties/AssemblyInfo.cs b/src/Microsoft.Extensions.Caching.SqlConfig.Tools/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..8d8d88195c --- /dev/null +++ b/src/Microsoft.Extensions.Caching.SqlConfig.Tools/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; +using System.Resources; + +[assembly: AssemblyMetadata("Serviceable", "True")] +[assembly: NeutralResourcesLanguage("en-us")] +[assembly: AssemblyCompany("Microsoft Corporation.")] +[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] +[assembly: AssemblyProduct("Microsoft .NET Extensions")] diff --git a/src/Microsoft.Extensions.Caching.SqlConfig.Tools/SqlQueries.cs b/src/Microsoft.Extensions.Caching.SqlConfig.Tools/SqlQueries.cs new file mode 100644 index 0000000000..110ee13caf --- /dev/null +++ b/src/Microsoft.Extensions.Caching.SqlConfig.Tools/SqlQueries.cs @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.Caching.SqlConfig.Tools +{ + internal class SqlQueries + { + private const string CreateTableFormat = "CREATE TABLE {0}(" + + // Maximum size of primary key column is 900 bytes (898 bytes from key + 2 additional bytes used by the + // Sql Server). In the case where the key is greater than 898 bytes, then it gets truncated. + // - Add collation to the key column to make it case-sensitive + "Id nvarchar(449) COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL, " + + "Value varbinary(MAX) NOT NULL, " + + "ExpiresAtTime datetimeoffset NOT NULL, " + + "SlidingExpirationInSeconds bigint NULL," + + "AbsoluteExpiration datetimeoffset NULL, " + + "CONSTRAINT pk_Id PRIMARY KEY (Id))"; + + private const string CreateNonClusteredIndexOnExpirationTimeFormat + = "CREATE NONCLUSTERED INDEX Index_ExpiresAtTime ON {0}(ExpiresAtTime)"; + + private const string TableInfoFormat = + "SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE " + + "FROM INFORMATION_SCHEMA.TABLES " + + "WHERE TABLE_SCHEMA = '{0}' " + + "AND TABLE_NAME = '{1}'"; + + public SqlQueries(string schemaName, string tableName) + { + if (string.IsNullOrEmpty(schemaName)) + { + throw new ArgumentException("Schema name cannot be empty or null"); + } + if (string.IsNullOrEmpty(tableName)) + { + throw new ArgumentException("Table name cannot be empty or null"); + } + + var tableNameWithSchema = string.Format( + "{0}.{1}", DelimitIdentifier(schemaName), DelimitIdentifier(tableName)); + CreateTable = string.Format(CreateTableFormat, tableNameWithSchema); + CreateNonClusteredIndexOnExpirationTime = string.Format( + CreateNonClusteredIndexOnExpirationTimeFormat, + tableNameWithSchema); + TableInfo = string.Format(TableInfoFormat, EscapeLiteral(schemaName), EscapeLiteral(tableName)); + } + + public string CreateTable { get; } + + public string CreateNonClusteredIndexOnExpirationTime { get; } + + public string TableInfo { get; } + + // From EF's SqlServerQuerySqlGenerator + private string DelimitIdentifier(string identifier) + { + return "[" + identifier.Replace("]", "]]") + "]"; + } + + private string EscapeLiteral(string literal) + { + return literal.Replace("'", "''"); + } + } +} diff --git a/src/Microsoft.Extensions.Caching.SqlConfig.Tools/project.json b/src/Microsoft.Extensions.Caching.SqlConfig.Tools/project.json new file mode 100644 index 0000000000..b8ffffdc12 --- /dev/null +++ b/src/Microsoft.Extensions.Caching.SqlConfig.Tools/project.json @@ -0,0 +1,41 @@ +{ + "version": "1.0.0-*", + "dependencies": { + "Microsoft.Extensions.CommandLineUtils": "1.1.0-*", + "Microsoft.Extensions.Logging": "1.1.0-*", + "Microsoft.Extensions.Logging.Console": "1.1.0-*", + "System.Data.SqlClient": "4.1.0-*" + }, + "description": "Command line tool to create tables and indexes in a Microsoft SQL Server database for distributed caching.", + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.0.0-*", + "type": "platform" + } + } + } + }, + "buildOptions": { + "outputName": "dotnet-sql-cache", + "emitEntryPoint": true, + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk", + "nowarn": [ + "CS1591" + ], + "xmlDoc": true + }, + "packOptions": { + "repository": { + "type": "git", + "url": "https://github.com/aspnet/DotNetTools" + }, + "tags": [ + "cache", + "distributedcache", + "sqlserver" + ] + } +} \ No newline at end of file