Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to restore a SQL Server backup in .NET Core

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
like image 752
JsAndDotNet Avatar asked May 09 '18 13:05

JsAndDotNet


2 Answers

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
like image 179
Dmitry Kolchev Avatar answered Sep 20 '22 05:09

Dmitry Kolchev


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.

like image 35
Walter Verhoeven Avatar answered Sep 21 '22 05:09

Walter Verhoeven