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