Convert a Console App to Class Library

The big difference, you can run a console app but you can’t run a class library. Usually, Console App has Program.cs file and will compile to .exe file, a class library by default is not executable and will compile to a .dll. In .NET Core console app can also compile to DLL.

A Console App has a flow and can be executed. A Class Library is just a collection of functions that you call from another source. Class libraries by themselves are pretty useless without something to call the functions inside them.

Console App has a static MAIN method that’s an entry point. When deciding to make a method or Static class, I always keep this in mind.

A static class can be used as a convenient container for sets of methods that just operate on input parameters and do not have to get or set any internal instance fields.

Create console app. Add a greeting method. Run the app. Switch the output type to Class Libarary. Right-Click on Project -> Properties;

Before;

After;

You have best of the both worlds.

How to load assembly in SSIS script task that isn’t in the GAC

The documented and recommended way to reference a custom assembly from an SSIS Script Task or Script Component is to install it in the Global Assembly Cache (GAC).  However this is not always possible or simple to do.

Here’s a simple workaround for loading an assembly from an arbitrary location.  The idea is to register an AppDomain.AssemblyResolve event handler.

The .NET Framework provides the AppDomain.AssemblyResolve event for applications that require greater control over assembly loading. By handling this event, your application can load an assembly into the load context from outside the normal probing paths, select which of several assembly versions to load, emit a dynamic assembly and return it, and so on. This topic provides guidance for handling the AssemblyResolve event.

Resolving Assembly Loads

Which does just what we need.  The question is where and how to wire it up.  The trick is to realize that .NET’s Just-in-Time (JIT) compilation is responsible for loading dependent assemblies, and therefore guarantees that the assemblies referenced in a type’s method won’t be loaded until just before the method is executed.  This means that in the type’s static constructor we can reliably wire up the AssemblyResolve event that will supply the referenced types.

When you create a SSIS Script Task or Script Component SSIS generates a class for you called ScriptMain, and you can wire-up the event in a Static Constructor for that type.  A Static Constructor is guaranteed to be called exactly once “before the first instance is created or any static members are referenced.”, so therefore before the dependent assemblies are loaded.

[Microsoft.SqlServer.Dts.Tasks.ScriptTask.SSISScriptTaskEntryPointAttribute]
public partial class ScriptMain : Microsoft.SqlServer.Dts.Tasks.ScriptTask.VSTARTScriptObjectModelBase
{
     static ScriptMain()
     {
         AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
     }
     static System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
     {
         if (args.Name.Contains("ssisHelper"))
         {
             string path = @"c:\temp\";
             return System.Reflection.Assembly.LoadFile(System.IO.Path.Combine(path, "ssisHelper.dll"));
         }
         return null;
     }
    . . .

You can then drop the referenced assemblies in a well-known location on the SSIS server, or use a package variable to refer to the location and pass that in to the Script Task or Script Component.

If you are having issues with getting the dll path from a variable, Here is the work around. Basically you need to remove the “static” qualifier from the constructor and the event handler declarations and just make them public. So: public static ScriptMain() becomes public ScriptMain() and public static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) becomes public Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args).

Here is the modified version;

[Microsoft.SqlServer.Dts.Tasks.ScriptTask.SSISScriptTaskEntryPointAttribute]
	public partial class ScriptMain : Microsoft.SqlServer.Dts.Tasks.ScriptTask.VSTARTScriptObjectModelBase
{
  //the assemblyl is not registered in GAC and referenced from a folder.
  public ScriptMain()
  {
    AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyReference);
  }

  public System.Reflection.Assembly CurrentDomain_AssemblyReference(object sender, ResolveEventArgs args)
  {
   //string path = @"C:\Dev\PathToSSISHelper\";
   string path = Dts.Variables["User::PathToSSISHelper"].Value.ToString();
   if (args.Name.Contains("SSISHelper"))
   {
      return System.Reflection.Assembly.LoadFrom(System.IO.Path.Combine(path, "SSISHelper.dll"));
   }
   return null;
  }
  //end of reflection
.....

}

If you try to read the assemblies from a network share, you might get (System.NotSupportedException). Therefore replaced the LoadFile(path) with the UnsafeLoadFrom(path) call as workaround. Please use it only for your own or other wellknown assemblies, not downloaded assemblies from unknown authors, because this would be a security issue.

Here is the working code, the referenced DLL is “System.Web.Helpers.dll” and the network share path gets configured in the user variable “LibPath” (VS 2015,SQL Server 2017):

public System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    string path = Variables.LibPath.ToString();

    if (args.Name.Contains("System.Web.Helpers"))
    {
        return System.Reflection.Assembly.UnsafeLoadFrom(System.IO.Path.Combine(path, "System.Web.Helpers.dll"));
    }

    return null;
}

/// <summary>
/// This method is called once, before rows begin to be processed in the data flow.
/// </summary>
public override void PreExecute()
{
    base.PreExecute();

    AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
}

Resource

Load custom assembly in SSIS

https://docs.microsoft.com/en-us/archive/blogs/dbrowne/how-to-load-an-assembly-in-a-ssis-script-task-that-isnt-in-the-gac

Using class library in SSIS Projects without GAC installation

I don’t want to add custom assembly in GAC. Here is my research.

There is no direct way to do that from integration services packages, because the only workaround is using CurrentDomain_AssemblyResolve function – loading an assembly from a location instead of installing it into GAC.

There are 3 ways for using custom dll’s on deployment:

  • Assigning DLL’s to the GAC
  • Using the AssemblyResolve Function
  • Copy all Dll’s to the sql server DTS Assemblies folder (example for SQL Server 2008: C:\Program Files\Microsoft SQL Server\100\DTS\Binn) and to the .Net framework assemblies folder.

There could be an interesting workaround by using a Web Service instead of direct dll, so you can create a web service which contains the methods you are using and add a Web reference instead of a Local assembly

Here is an example.

This might be your starting point.

Using custom DLL in script task

Resources

https://docs.microsoft.com/en-us/answers/questions/92003/unable-to-reference-custom-class-library-in-ssis-s.html

https://www.codeproject.com/Articles/895028/Refering-dlls-in-SSIS-Script-Task

https://www.dotnetspider.com/resources/45645-How-to-use-custom-DLL-in-SSIS-Package.aspx

Using email template to compose and send emails

This is a simple prototype to compose and send HTML emails using a sample template.

Here is the template;

String emailTemplate = @"
<html lang=""en"">
    <head>    
        <meta content=""text/html; charset=utf-8"" http-equiv=""Content-Type"">
        <title>
            {0}
        </title>
        <style type=""text/css"">
            HTML{background-color: #e8e8e8;}
            .document-table{font-size: 12px; padding: 3px; border-collapse: collapse; border-spacing: 0;}
            .document-table .description{color: #505050;}
            .document-table td{border: 1px solid #D1D1D1; background-color: #F3F3F3; padding: 0 10px;}
            .document-table th{border: 1px solid #424242; color: #FFFFFF;text-align: left; padding: 0 10px;}
            .green{background-color: #6B9852;}
            .wrapper { margin-bottom: 20px;}
        </style>
    </head>
    <body>
        <div class=""wrapper"">
            Team,
        </div>
        <div class=""wrapper"">
            SampleDb data pull job {1} last night. The job is configured to run on {2} basis.
        </div>
        <div class=""wrapper"">
            SampleDb database is scanned between {3} for new documents. Following new document have been found and their data is copied over to the target database.
        </div>
        <div class=""wrapper"">
            <table class=""document-table"">
                <thead>
                    <tr>
                        <th class=""green"">AccountNumber</th>
                        <th class=""green"">FYQ</th>
                        <th class=""green"">DateAdded</th>
                        <th class=""green"">RowStatus</th>
                    </tr>
                </thead>
                <tbody>
                    {4}
                </tbody>
            </table>
        </div>
        <div class=""wrapper"">
            <h5><br /><br />This inbox is unattended. please do not reply to this email. This is an auto-generated message.</h5>
        </div>
    </body>
</html>
";

We can use dynamic and ExpandoObject to create data. Alternatively you can grab data from database;

var list = new List<dynamic>();
dynamic row = new ExpandoObject();
row.AccountNumber = "XYZ-100";
row.FYQ = "Q1 FY 1999";
row.DateAdded = "1999-07-28 19:38:00.000";
row.RowStatus = "processed";
list.Add(row);

We need to fill template placeholders;

StringBuilder sb = new StringBuilder();           
list.ForEach(x =>
{
   sb.Append("<tr>");
   sb.Append($"<td class='description'>{x.AwardNumber}</td>");
   sb.Append($"<td>{x.FYQ}</td>");
   sb.Append($"<td>{x.DateAdded}</td>");
   sb.Append($"<td>{x.RowStatus}</td>");
   sb.Append("</tr>");
});

replace all placeholders in HTML template;

string emailBody =
    emailTemplate.Replace("{0}", "A demo email body title")
    .Replace("{1}", "succeeded")
    .Replace("{2}", "yearly")
    .Replace("{3}", "01/01/1999 23.59.59 - 11/2/2021 02:00:00 AM")
    .Replace("{4}", sb.ToString());
               

Compose and send email out;

MailMessage mailMessage = new MailMessage();
mailMessage.From = new MailAddress("FromAdam@mail.com", "Sample Data Pull");
mailMessage.To.Add("ToJoe@mail.com");
mailMessage.Body = emailTemplate;
mailMessage.Subject = "Test email";
mailMessage.IsBodyHtml = true;
mailMessage.SubjectEncoding = Encoding.UTF8;
mailMessage.BodyEncoding = Encoding.UTF8;

//send email
using (var client = new SmtpClient("smtp-host-address"))
{
   //if true, user/pwd required, false network credentials will be used
   client.UseDefaultCredentials = true;
   if (client.UseDefaultCredentials)
   {
   client.Credentials = new NetworkCredential("FromAdmam@mail.com", "AdamSecretPassword");
   }
   client.EnableSsl = true;
   client.DeliveryMethod = SmtpDeliveryMethod.Network;
   client.Timeout = 3000;      //3 seconds
   client.Send(mailMessage);
}

Run your program and check your inbox. You will see HTML email with CSS style applied. The HTML table element is replace with our created data.

Have fun!