THE SQL Server Blog Spot on the Web

Welcome to SQLblog.com - The SQL Server blog spot on the web Sign in | |
in Search

Piotr Rodak

if datepart(dw, getdate()) in (6, 7)
    use pubs;



Running SSIS packages from C#

Most of the developers and DBAs know about two ways of deploying packages: You can deploy them to database server and run them using SQL Server Agent job or you can deploy the packages to file system and run them using dtexec.exe utility. Both approaches have their pros and cons. However I would like to show you that there is a third way (sort of) that is often overlooked, and it can give you capabilities the ‘traditional’ approaches can’t.

I have been working for a few years with applications that run packages from host applications that are implemented in .NET. As you know, SSIS provides programming model that you can use to implement more flexible solutions. SSIS applications are usually thought to be batch oriented, with fairly rigid architecture and processing model, with fixed timeframes when the packages are executed to process data. It doesn’t to be the case, you don’t have to limit yourself to batch oriented architecture. I have very good experiences with service oriented architectures processing large amounts of data. These applications are more complex than what I would like to show here, but the principle stays the same: you can execute packages as a service, on ad-hoc basis. You can also implement and schedule various signals, HTTP calls, file drops, time schedules, Tibco messages and other to run the packages. You can implement event handler that will trigger execution of SSIS when a certain event occurs in StreamInsight stream.

This post is just a small example of how you can use the API and other features to create a service that can run SSIS packages on demand.

I thought it might be a good idea to implement a restful service that would listen to requests and execute appropriate actions. As it turns out, it is trivial in C#. The application is implemented as console application for the ease of debugging and running. In reality, you might want to implement the application as Windows service. To begin, you have to reference namespace System.ServiceModel.Web and then add a few lines of code:

  1. Uri baseAddress = new Uri("http://localhost:8011/");
  2.  
  3.             WebServiceHost svcHost = new WebServiceHost(typeof(PackRunner), baseAddress);
  4.              
  5.             try
  6.             {
  7.                 svcHost.Open();
  8.  
  9.                 Console.WriteLine("Service is running");
  10.                 Console.WriteLine("Press enter to stop the service.");
  11.                 Console.ReadLine();
  12.  
  13.                 svcHost.Close();
  14.             }
  15.             catch (CommunicationException cex)
  16.             {
  17.                 Console.WriteLine("An exception occurred: {0}", cex.Message);
  18.                 svcHost.Abort();
  19.             }

The interesting lines are 3, 7 and 13. In line 3 you create a WebServiceHost object. In line 7 you start listening on the defined URL and then in line 13 you shut down the service.

As you have noticed, the WebServiceHost constructor is accepting type of an object (here: PackRunner) that will be instantiated as singleton and subsequently used to process the requests. This is the class where you put your logic, but to tell WebServiceHost how to use it, the class must implement an interface which declares methods to be used by the host. The interface itself must be ornamented with attribute ServiceContract.

  1. [ServiceContract]
  2.     public interface IPackRunner
  3.     {
  4.         [OperationContract]
  5.         [WebGet(UriTemplate = "runpack?package={name}")]
  6.         string RunPackage1(string name);
  7.  
  8.         [OperationContract]
  9.         [WebGet(UriTemplate = "runpackwithparams?package={name}&rows={rows}")]
  10.         string RunPackage2(string name, int rows);
  11.     }

Each method that is going to be used by WebServiceHost has to have attribute OperationContract, as well as WebGet or WebInvoke attribute. The detailed discussion of the available options is outside of scope of this post. I also recommend using more descriptive names to methods Smile.

Then, you have to provide the implementation of the interface:

  1. public class PackRunner : IPackRunner
  2.     {
  3.         ...

There are two methods defined in this class. I think that since the full code is attached to the post, I will show only the more interesting method, the RunPackage2.

 

  1. /// <summary>
  2. /// Runs package and sets some of its variables.
  3. /// </summary>
  4. /// <param name="name">Name of the package</param>
  5. /// <param name="rows">Number of rows to export</param>
  6. /// <returns></returns>
  7. public string RunPackage2(string name, int rows)
  8. {
  9.     try
  10.     {
  11.         string pkgLocation = ConfigurationManager.AppSettings["PackagePath"];
  12.  
  13.         pkgLocation = Path.Combine(pkgLocation, name.Replace("\"", ""));
  14.  
  15.         Console.WriteLine();
  16.         Console.WriteLine("Calling package {0} with parameter {1}.", name, rows);
  17.         
  18.         Application app = new Application();
  19.         Package pkg = app.LoadPackage(pkgLocation, null);
  20.  
  21.         pkg.Variables["User::ExportRows"].Value = rows;
  22.         DTSExecResult pkgResults = pkg.Execute();
  23.         Console.WriteLine();
  24.         Console.WriteLine(pkgResults.ToString());
  25.         if (pkgResults == DTSExecResult.Failure)
  26.         {
  27.             Console.WriteLine();
  28.             Console.WriteLine("Errors occured during execution of the package:");
  29.             foreach (DtsError er in pkg.Errors)
  30.                 Console.WriteLine("{0}: {1}", er.ErrorCode, er.Description);
  31.             Console.WriteLine();
  32.             return "Errors occured during execution. Contact your support.";
  33.         }
  34.         
  35.         Console.WriteLine();
  36.         Console.WriteLine();
  37.         return "OK";
  38.     }
  39.     catch (Exception ex)
  40.     {
  41.         Console.WriteLine(ex);
  42.         return ex.ToString();
  43.     }
  44. }

 

The method accepts package name and number of rows to export. The packages are deployed to the file system. The path to the packages is configured in the application configuration file. This way, you can implement multiple services on the same machine, provided you also configure the URL for each instance appropriately.

To run a package, you have to reference Microsoft.SqlServer.Dts.Runtime namespace. This namespace is implemented in Microsoft.SQLServer.ManagedDTS.dll which in my case was installed in the folder “C:\Program Files (x86)\Microsoft SQL Server\100\SDK\Assemblies”. Once you have done it, you can create an instance of Microsoft.SqlServer.Dts.Runtime.Application as in line 18 in the above snippet. It may be a good idea to create the Application object in the constructor of the PackRunner class, to avoid necessity of recreating it each time the service is invoked. Then, in line 19 you see that an instance of Microsoft.SqlServer.Dts.Runtime.Package is created. The method LoadPackage in its simplest form just takes package file name as the first parameter.

Before you run the package, you can set its variables to certain values. This is a great way of configuring your packages without all the hassle with dtsConfig files. In the above code sample, variable “User:ExportRows” is set to value of the parameter “rows” of the method.

Eventually, you execute the package. The method doesn’t throw exceptions, you have to test the result of execution yourself. If the execution wasn’t successful, you can examine collection of errors exposed by the package. These are the familiar errors you often see during development and debugging of the package. I you run the package from the code, you have opportunity to persist them or log them using your favourite logging framework.

The package itself is very simple; it connects to my AdventureWorks database and saves number of rows specified in variable “User::ExportRows” to a file.

You should know that before you run the package, you can change its connection strings, logging, events and many more.

I attach solution with the test service, as well as a project with two test packages.

To test the service, you have to run it and wait for the message saying that the host is started.

Then, just type (or copy and paste) the below command to your browser.

http://localhost:8011/runpackwithparams?package=%22ExportEmployees.dtsx%22&rows=12

When everything works fine, and you modified the package to point to your AdventureWorks database, you should see "OK” wrapped in xml:

I stopped the database service to simulate invalid connection string situation. The output of the request is different now:

And the service console window shows more information:

As you see, implementing service oriented ETL framework is not a very difficult task. You have ability to configure the packages before you run them, you can implement logging that is consistent with the rest of your system. In application I have worked with we also have resource monitoring and execution control. We don’t allow to run more than certain number of packages to run simultaneously. This ensures we don’t strain the server and we use memory and CPUs efficiently.

The attached zip file contains two projects. One is the package runner. It has to be executed with administrative privileges as it registers HTTP namespace. The other project contains two simple packages.

This is really a cool thing, you should check it out!

Published Monday, April 09, 2012 10:52 PM by Piotr Rodak

Attachment(s): ServiceETLDemo.zip

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

 

ashish said:

Do we have option to run it asynchronously?..I mean do we have something like BeginExecute()?

April 10, 2012 1:03 AM
 

Piotr Rodak said:

Sure you can. Since the service runs as singleton, you can have whole asynchronous execution framework implemented. You just enqueue execution of the package into some sort of a queue and return.

April 10, 2012 3:17 AM
 

Sid said:

This looks fantastic and I'm trying your sample but I've hit a problem and I don't readily see the solution.  I have the SSIS package working and it runs and creates a file if I run it through in Visual Studio but I can't get it to work from the service.  I'm getting this erorr from the console:

Calling package ExportEmployees.dtsx with parameter 12.

Microsoft.SqlServer.Dts.Runtime.DtsRuntimeException: The package failed to load

due to error 0xC0011008 "Error loading from XML. No further detailed error infor

mation can be specified for this problem because no Events object was passed whe

re detailed error information can be stored.". This occurs when CPackage::LoadFr

omXML fails.

---> System.Runtime.InteropServices.COMException: The package failed to load du

e to error 0xC0011008 "Error loading from XML. No further detailed error informa

tion can be specified for this problem because no Events object was passed where

detailed error information can be stored.". This occurs when CPackage::LoadFrom

XML fails.

  at Microsoft.SqlServer.Dts.Runtime.Wrapper.ApplicationClass.LoadPackage(Strin

g FileName, Boolean loadNeutral, IDTSEvents90 pEvents)

  at Microsoft.SqlServer.Dts.Runtime.Application.LoadPackage(String fileName, I

DTSEvents events, Boolean loadNeutral)

  --- End of inner exception stack trace ---

  at Microsoft.SqlServer.Dts.Runtime.Application.LoadPackage(String fileName, I

DTSEvents events, Boolean loadNeutral)

  at Microsoft.SqlServer.Dts.Runtime.Application.LoadPackage(String fileName, I

DTSEvents events)

  at SSISRunner.PackRunner.RunPackage2(String name, Int32 rows) in L:\ZipStuff\

ServiceETLDemo\SSISRunner\SSISRunner\Program.cs:line 118

http://localhost:8011/runpackwithparams?package=%22ExportEmployees.dtsx%22&rows=

12

I'm running everything locally on my workstation on which I'm an admin.

I've probably missed something relatively simple but could you please help.

Any help is greatly appreciated.

September 12, 2012 8:55 AM
 

Sid said:

Ok -- I got it  -- the console app references a 2005 library:

C:\Program Files\Microsoft SQL Server\90\SDK\Assemblies\Microsoft.SQLServer.ManagedDTS.dll

and I'm using a 2008 SSIS package so just switch the reference to the same dll in the \100 subdirectory and it works as expected.

Sorry for the confusion,

This article clarified it for me:

http://sqlaj.wordpress.com/2010/01/12/t-sql-tuesday-002-a-puzzling-situation-the-package-failed-to-load-due-to-error-0xc0011008/

thanks again for this great article!!!

My head is already spinning with ideas on how to apply this technology.

September 12, 2012 11:06 AM

Leave a Comment

(required) 
(required) 
Submit
Powered by Community Server (Commercial Edition), by Telligent Systems
  Privacy Statement