Click here to Skip to main content
Click here to Skip to main content

Ready-to-use Mass Emailing Functionality with C#, .NET 2.0, and Microsoft® SQL Server 2005 Service Broker

By , 7 Sep 2006
 

Abstract

This paper demonstrates an extensible mass emailing framework (Smart Mass Email SME). The demo implementation uses cutting edge .NET technologies available today, such as C#, .NET 2.0, Microsoft® SQL Server 2005 Service Broker, MS Provider Pattern, Enterprise Library January 2006 etc.

Introduction

I thought of writing this article in a different way, just like a conversation session in MSN Messenger / Windows Live Messenger.

Alex: Hi. I am writing a new article on Smart Mass Emailing (SME) feature in .NET 2.0.

Bob: Sounds interesting. Tell me a little bit more about it.

A: Mass Emailing is a very common feature for lots of websites or applications. I have written an extensible and pluggable framework for this SME feature. The demo application that comes with the article provides examples of running SME as a Windows Service and also as a website. The second feature is very useful when you want to run SME in a Shared Web Environment where Windows Services is not available. SME will use the same technique presented in this article, “Simulate a Windows Service using ASP.NET to run scheduled jobs”.

The demo application demonstrates ready to use mass emailing functionality for both Microsoft® SQL Server 2000 and Microsoft® SQL Server 2005. For Microsoft® SQL Server 2005, I have used Service Broker, and for Microsoft® SQL Server 2000, I have used a normal database table. And to make the framework pluggable and extensible, I have used the Microsoft® Provider Pattern.

Technologies Used in SME

B: I am aware of the Microsoft® Provider Design Pattern, but I have heard very little about Microsoft® SQL Server Service Broker. Please, tell me a bit more about it.

A: For a refresher on the Provider Pattern, have a look at my previous article “Flexible and Plug-in-based .NET Applications Using the Provider Pattern”.

Microsoft® SQL Server 2005 Service Broker

And Service Broker is a new feature of Microsoft® SQL Server 2005. It offers asynchronous messaging support, and is tightly integrated with the SQL Server database engine. Service Broker provides a new, queue-based durable messaging framework which can send and receive messages between service endpoints, such as server instances, databases, and .NET 2.0 clients. Messages can be up to 2 GB in size, and can use the varbinary or varbinary(max) data types. I have blogged some links and resources for SSB. Looking at these links will be good starting point.

SME Framework

B: Very interesting. Can’t wait to learn more about the SME Framework.

A: I am very excited to share it as well. Have a look at the following diagrams.

Fig: Smart Mass Email Workflow

From the diagram, you will notice that multiple providers are used to accomplish this task. SME involves five different providers in its framework, and I am going to describe them one by one soon.

To understand the SME Framework, we need to understand the EmailMessage and Template objects, and then all five providers in detail.

EmailMessage Object in the SME Framework

The EmailMessage object is simply a business object which holds the email details. I had to create and use this object to have the serialization feature in the framework.

Fig Business Object: EmailMessage

Template Object in the SME Framework

The Template object is simply another business object which holds the email template.

Fig Business Object: Template

A client application simply needs to create an EmailMessage, get it templated, and send it to the specified queue.

An implementation of Provider Pattern in SME

Fig Smart Mass Email Providers

SME Providers

EmailQueueProvider: This provider provides email queuing functionality.

public abstract class EmailQueueProvider : ProviderBase {
  public abstract bool Send(EmailMessage message); 
}

EmailTemplateProvider: This provider finds a template and serves as a templated message.

public abstract class EmailTemplateProvider : ProviderBase { 
  public abstract Template GetTemplate(string templateName);
  public abstract EmailMessage GetTemplatedMessage(string templateName, 
         EmailMessage message, StringDictionary namevalue);
}

EmailDequeueProvider: This provider provides email dequeuing functionality.

public abstract class EmailDeQueueProvider : ProviderBase {
  public abstract TList<EmailMessage> Recieve();
  public abstract bool Delete(EmailMessage message);
}

EmailDispatchProvider: Provides email dispatching functionality using SMTPClient or SMTPMail.

public abstract class EmailDispatchProvider : ProviderBase { 
  public abstract bool Dispatch(TList<EmailMessage> list);
}

ProcessFailureProvider: Processes any of the messages that fail during the email dispatching. For example, compares the maximum retry and the number of retry attempts of a message, and decides whether to queue the message again or to delete it permanently.

public abstract class ProcessFailureProvider : ProviderBase {
  public abstract void Process(TList<EmailMessage> list); 
}

Configuration information: As described in the Microsoft® Provider specification, after the concrete provider is implemented, it must be described in the configuration section. The beauty of the Provider Pattern is that the appropriate provider is instantiated at run time from information contained in the configuration file, and you may define an unlimited number of providers.

<smartMassEmail.providers>
  <emailQueue defaultProvider="Sql2005EmailQueueProvider">
    <providers>
      <add name="Sql2000EmailQueueProvider" 
           type="SmartMassEmail.ImplementedProviders.Sql2000EmailQueueProvider, 
                 SmartMassEmail.ImplementedProviders" />
      <add name="Sql2005EmailQueueProvider" 
           type="SmartMassEmail.ImplementedProviders.Sql2005EmailQueueProvider, 
                 SmartMassEmail.ImplementedProviders" />
    </providers>
  </emailQueue>
</ smartMassEmail.providers>

In the above section, we have added the Sql2000EmailQueueProvider and Sql2000EmailQueueProvider to our providers list.

The use of Enterprise Library in the SME Framework: SME uses Enterprise Library January 2006 for data access, caching, logging, and exception handling.

Email Queue and Template

B: So the process starts with queuing EmailMessage, if I am correct?

A: Yes, you are. The client application needs to create an EmailMessage, object and can use the EmailTemplateProvider to get a templated message before queuing.

//Creating a EmailMessage Object
EmailMessage message = new EmailMessage();
message.ID = Guid.NewGuid();
message.EmailSubject = "Subject";
message.EmailTo = "skhan@veraida.com";
message.EmailFrom = "psteele@veraida.com";
…..
message.EmailBody = "Test email";
…..
message.MaximumRetry = 3;
message.NumberOfRetry = 0;
…..
//Getting the NameValue pair to pass in the TemplateProvider
StringDictionary namevalue = GetPreparedNameValue(message); 
//Get Templated EmailMessage Object
EmailMessage templatedmessage = 
   EmailTemplate.GetTemplatedMessage("GenericEmailTemplate", 
                                      message, namevalue);
//Finally Queue the Email
EmailQueue.Send(templatedmessage);

Here, you will notice that I simply created an EmailMessage object, and then this piece of code returns a name-value pair that will be used in the template.

StringDictionary namevalue = GetPreparedNameValue(message);

private StringDictionary GetPreparedNameValue( EmailMessage message ) {
    //prepare namevalue for the template.
    StringDictionary dict = new StringDictionary();
    dict.Add("[recievername]", "Shahed");
    dict.Add("[sendername]", "Khan");
    dict.Add("[body]", message.EmailBody);
    return dict;
}

The template used in this demo looks similar to the code below, where the [recievername], [body], and [sendername] will be replaced by the name-value pair, by matching the name and replacing it with the value.

<html>
<body>
Hi <b>[recievername]</b>
This is a Generic Mail.
[body]
Regards,
[sendername]
</body>
</html>

And the following piece of code gets the templated message back.

EmailMessage templatedmessage = 
     EmailTemplate.GetTemplatedMessage("GenericEmailTemplate", 
                                        message, namevalue);

What this does ‘under the hood’, is that it uses the FileSystemEmailTemplateProvider that is available within the demo application to get the template, by name, from a predefined path. It then reads the file and converts it to an SME Template object. Then it replaces the TemplateBody with the name-value supplied, and returns the templated EmailMessage object.

//Get template from filesystem and return a Template Object
public override SmartMassEmail.Entities.Template 
                GetTemplate(string templateName) {
  string path = 
    ConfigurationManager.AppSettings.Get("TemplateFolderPath").ToString();
  string fileContents;

  using (System.IO.StreamReader sr = new System.IO.StreamReader(
                  string.Format(@"{0}{1}.txt",path,templateName)))
  {
    fileContents = sr.ReadToEnd();
  }
  SmartMassEmail.Entities.Template template = 
                 new SmartMassEmail.Entities.Template();
  template.TemplateName = templateName;
  template.TemplateBody = fileContents;
  return template;
}

//Gets Templated EmailMessage.
public override SmartMassEmail.Entities.EmailMessage 
       GetTemplatedMessage(string templateName, 
       SmartMassEmail.Entities.EmailMessage message, 
       StringDictionary namevalue){
  //Getting the predefined template.
  Template template = GetTemplate(templateName);
  if (message != null)
    ….
  foreach (DictionaryEntry de in namevalue)
  {
    template.TemplateBody = 
             template.TemplateBody.Replace(de.Key.ToString(), 
                                           de.Value.ToString());
  }
  message.EmailBody = template.TemplateBody;
  …..
  return message;
}

The returned EmailMessage is then queued for dispatch, using the following piece of code:

EmailQueue.Send(templatedmessage);

B: OK, I got you. Here, you have demonstrated how to turn an EmailMessage into a templated email message using the EmailTemplateProvider, and then you have shown how to queue the email.

In this case, you have used FileSystemEmailTemplateProvider for templating the email message, but anyone can also write their own provider inheriting from the EmailTemplateProvider too.

A: Yes you are correct. The SME framework provides the ability to use a templated message, and the demo application implements the FileSystemEmailTemplateProvider which reads a template file from the file system. Different providers can also be written which can provide the ability to read a template from a database or a similar source.

Email Dequeue, Dispatch, and Process Failures

B: OK, I understand. Let’s go back a bit. You mentioned previously that the demo application demonstrates ready-to-use mass emailing functionality for Microsoft® SQL Server 2000 and Microsoft® SQL Server 2005, can you please clarify this further?

A: Sure. What I meant is I have written a couple of providers for SME. I have written providers for Microsoft SQL Server 2005, using Service Broker Technology. So, emails will be queued in Service Broker Queues asynchronously, and then the email dispatch framework will read from the queues and deliver the emails. On the other hand, I provided a solution for users who do not have access to Microsoft SQL Server 2005. They can use Microsoft SQL Server 2000, where the message is added as a row in the database table and later dispatched from there. Let me discuss both implementations one after another.

SME Providers for Microsoft® SQL Server 2005

To make the MessageType, Queue, and Services components ready, I had to run the following T-SQL script on the database.

/****** Sql script ******/ 
USE [SmartMassEmailDB2005]
GO
/****** Object: MessageType [SMEMessageType] ******/
CREATE MESSAGE TYPE [SMEMessageType] AUTHORIZATION 
       [dbo] VALIDATION = WELL_FORMED_XML

/****** Object: ServiceContract [SMEContract] ******/
CREATE CONTRACT [SMEContract] AUTHORIZATION [dbo] 
     ([SMEMessageType] SENT BY INITIATOR)

/****** Object: ServiceQueue [dbo].[SMEPostQueue] ******/
CREATE QUEUE [dbo].[SMEPostQueue] WITH STATUS = ON , 
       RETENTION = OFF ON [PRIMARY] 

/****** Object: ServiceQueue [dbo].[SMEResponseQueue] ******/
CREATE QUEUE [dbo].[SMEResponseQueue] WITH STATUS = ON , 
       RETENTION = OFF ON [PRIMARY] 

/****** Object: BrokerService [SMEPostingService] ******/
CREATE SERVICE [SMEPostingService] AUTHORIZATION [dbo] 
       ON QUEUE [dbo].[SMEPostQueue] 

/****** Object: BrokerService [SMEService] ******/
CREATE SERVICE [SMEService] AUTHORIZATION [dbo] ON 
       QUEUE [dbo].[SMEPostQueue] ([SMEContract])
Sql2005EmailQueueProvider

This provider queues email messages to the Service Broker.

public override bool Send(EmailMessage message)
{
  if (message != null)
  {
    string xml = "";
    using (MemoryStream stream = new MemoryStream())
    {
      System.Xml.Serialization.XmlSerializer x = new 
        System.Xml.Serialization.XmlSerializer(message.GetType());
      x.Serialize(stream, message);
      xml = ConvertByteArrayToString(stream.ToArray());
    }
    string sql = 
           string.Format (@"DECLARE @dialog_handle UNIQUEIDENTIFIER; 
                            BEGIN DIALOG CONVERSATION @dialog_handle
                            FROM SERVICE [SMEPostingService]
                            TO SERVICE 'SMEService'
                            ON CONTRACT [SMEContract] ;

                            -- Send message on dialog conversation
                            SEND ON CONVERSATION @dialog_handle
                            MESSAGE TYPE [SMEMessageType]
                            ('{0}') ;
                            End Conversation @dialog_handle
                            With cleanup", xml);

    string connectionString = 
      ConfigurationManager.ConnectionStrings[
     "SmartMassEmailConnectionString2005"].ConnectionString;
    SqlConnection conn = new SqlConnection(connectionString);
    SqlCommand cmd = null;
    try
    {
      conn.Open();
      cmd = conn.CreateCommand();
      cmd.CommandText = sql;
      cmd.Transaction = conn.BeginTransaction();
      cmd.ExecuteNonQuery();
      cmd.Transaction.Commit();
      conn.Close();
      return true;
    }
    …..
  }

As you have noticed, first of all the EmailMessage object is serialized using the XmlSerializer, and then queued to the database using the T-SQL SEND command.

Sql2005EmailDequeueProvider

This provider dequeues email messages from the Service Broker.

m_queueSchema = "dbo";
m_queueName = "SMEPostQueue";
private SqlCommand CreateReceiveCommand()
{
    SqlCommand cmd = m_connection.CreateCommand();
    cmd.CommandText = @"WAITFOR (RECEIVE TOP (10) *, " + 
                      @"CONVERT( NVARCHAR(max), " + 
                      @"message_body ) as mgb FROM [" +
                      m_queueSchema.Replace("]", "]]") + "].[" +
                      m_queueName.Replace("]", "]]") + "]), 
                      TIMEOUT @timeout";
    cmd.Parameters.Add("@timeout", System.Data.SqlDbType.Int);
    cmd.CommandTimeout = 0;
    return cmd;
}

The above command is executed to receive the message from the queue. And after receiving the message, it is turned back into an EmailMessage object, and later passed to the EmailDispatchProvider.

string xml = reader["mgb"].ToString();
if (xml != null)
{
    byte[] bytes = Encoding.Unicode.GetBytes(xml);
    EmailMessage message = new EmailMessage();
    message = (EmailMessage)LoadFromXml(message, bytes); 
}
Dotnet2EmailDispatchProvider

This provider uses an SmtpClient to send an email to the client. It loops through the EmailMessage list and sends each email. The error handler catches the exception, keeps the application rolling and sending other emails, but adds the failed emails to the failure list, which is then passed on to the ProcessFailureProvider for further processing.

public override bool Dispatch(TList<EmailMessage> list)
{
    TList<EmailMessage> failurelist = new TList<EmailMessage>();
    //Gets SmtpSettins from the config
    SmtpSetting site = GetSmtpSetting();
    …..
    SmtpClient client = new SmtpClient();
    …..
    foreach (EmailMessage em in list)
    {
        try
        {
            MailMessage message = new MailMessage();
            message.From = new MailAddress(em.EmailFrom);
            message.To.Add(new MailAddress(em.EmailTo));
            message.Subject = em.EmailSubject;
            message.Body = em.EmailBody;
            …..
            message.IsBodyHtml = em.IsHtml;
            …..
            client.Send(message);
            if (connectionLimit != -1 && ++sentCount >= connectionLimit)
            {
                Thread.Sleep(new TimeSpan(0, 0, 0, 
                       site.WaitSecondsWhenConnectionLimitExceeds, 0));
                sentCount = 0;
            }
            // on error, loop so to continue sending other email.
        }
        catch (Exception e)
        {
            …..
            // Add it to the failure list
            failurelist.Add(em);
        }
        ++totalSent;
    }
    if (failurelist.Count > 0)
    {
        //Process failures by passing the failed 
        //messages to the ProcessFailure provider
        ProcessFailedMessages(failurelist);
        return false;
    }
    return true;
}
Sql2005ProcessFailureProvider

This provider loops through each of the failed messages, and compares the NumberOfRetry < MaximumRetry defined by the message, and then will queue the message again, or discard it completely.

public override void Process(TList<EMAILMESSAGE> list)
{
    …..
    foreach (EmailMessage message in list)
    {
        message.NumberOfRetry = message.NumberOfRetry + 1;
        // put the message in queue again if maximum retry not exceeded
        if (message.NumberOfRetry < message.MaximumRetry)
        {
            string xml = "";
            using (MemoryStream stream = new MemoryStream())
            {
                ……
                xml = ConvertByteArrayToString(stream.ToArray());
            }
            string sql =
                string.Format(@"DECLARE @dialog_handle UNIQUEIDENTIFIER; 
                                BEGIN DIALOG CONVERSATION @dialog_handle
                                FROM SERVICE [SMEPostingService]
                                TO SERVICE 'SMEService'
                                ON CONTRACT [SMEContract] ;
                
                                -- Send message on dialog conversation
                                SEND ON CONVERSATION @dialog_handle
                                MESSAGE TYPE [SMEMessageType]
                                ('{0}') ;

                                End Conversation @dialog_handle
                                With cleanup", xml); ………
            try
            {
                ………
                cmd.Transaction = conn.BeginTransaction();
                cmd.ExecuteNonQuery();
                cmd.Transaction.Commit();
                conn.Close();
            }
            catch (Exception x)
            {
            …..

B: It is clear to me now how you have used the Microsoft® SQL Server 2005 Service Broker Messaging Framework to create a reliable mass emailing functionality, but how did you implement this in Microsoft® SQL Server 2000, as it lacks the messaging engine functionality?

A: Yes, you are right. SQL Server 2000 does not have the messaging engine, and I had to simulate the messaging feature used in SQL Server 2005, using tables and stored procedures.

SME Providers for MSSQL 2000

Sql2000EmailQueueProvider

This provider simply adds a row to the table EmailMessage with a status = 0 (pending), to be picked up by the EmailDeQueueProvider.

SQL2000EmailDeQueueProvider

This provider uses the following stored procedure to receive pending EmailMessages.

SELECT TOP 5 [ID], [ChangeStamp], [Priority], [Status], 
             [NumberOfRetry], [RetryTime], [MaximumRetry], 
             [ExpiryDatetime], [ArrivedDateTime], [SenderInfo], 
             [EmailTo], [EmailFrom], [EmailSubject], 
             [EmailBody], [EmailCC], [EmailBCC], [IsHtml]
FROM dbo.[EmailMessage]
WHERE NumberOfRetry < MaximumRetry and 
      RetryTime < getdate() and Status = 0 
ORDER BY Priority,RetryTime

B: I notice here that priority is included, which was not there in your Microsoft® SQL Server 2005 implementation.

A: Yes, I was coming to that point; to keep the implementation of Microsoft® SQL Server 2005 simple, I ignored the message priority option. But this blog post, Message Priority, will point you towards the right direction, and some modification to the existing provider will do the trick.

The Microsoft SQL Server 2000 providers of this demo are compatible with Message Priority, and as you have noticed in the stored procedure, the messages are received ordered by Priority, RetryTime.

B: True, and I notice another property RetryTime in your Microsoft® SQL Server 2000 implementation. Will you please elaborate on this?

A: Sure. In this implementation when you queue a message, you can define when to dispatch the message. Also, on the failure of delivery of a message, you can define a new RetryTime for the message to be delivered again. For example, if you want to dispatch the email again after 2 days, just put the RetryTime as such. The EmailDeQueueProvider will pick it up only when RetryTime < getdate().

SMTPDotnet2EmailDispatchProvider

I have used the same dispatch provider as the Microsoft® SQL Server 2005 implementation here, to dispatch the messages.

SMTPDotnet1EmailDispatchProvider

In the demo application, you will also find this dispatch provider which uses SmtpMail, which is the older way of sending mail. However, the .NET Framework 2.0 recommends using the SmtpClient instead of SmtpMail which is marked as obsolete now. I have written this provider using SmtpMail, as initially, I had planned to write a SME for .NET 1.1 as well, but this did not eventuate.

SQL2000ProcessFailureProvider

This provider loops through the failed messages, and compares if NumberOfRetry is less than MaximumRetry. If this condition is true, 10 minutes are added to RetryTime, and the status is changed back to Pending. Otherwise, the message is deleted.

public override void Process(TList<EmailMessage> list )
{ 
    foreach (EmailMessage em in list)
    {
        //Check NumberOfRetry< MaximumRetry
        if (em.NumberOfRetry < em.MaximumRetry)
        {
            //Change the status back to pending
            em.Status = (int)EmailMessage.EmailMessageStatus.Pending;
            em.NumberOfRetry = em.NumberOfRetry + 1;
            //ReTry again after 10 min
            em.RetryTime = em.RetryTime.AddMinutes(10);
            DataRepository.EmailMessageProvider.Update(em);
        }
        else 
        {
            //Delete the Message
            DataRepository.EmailMessageProvider.Delete(em);
        }
    }
}

B: So, what I understand so far is, SME is an extensible framework for mass emailing, and you have already created providers for Microsoft® SQL Server 2005 and Microsoft® SQL Server 2000. However, end users can modify the existing providers, or implement their own providers if they wish. For example, if someone wants to queue the email messages in Microsoft ® Message Queuing (MSMQ), they simply have to write a provider for that, inheriting from the EmailQueueProvider of the SME Framework. To implement DeQueue and ProcessFailures methods, two more providers will have to be written in the same way.

A: You are correct. The whole idea of using the Microsoft® Provider Pattern is to achieve this flexibility. Now, I'll discuss a bit on running SME as an ASP.NET application and as a Windows Service.

Running the EmailDispatcher as an ASP.NET Application or as a Windows Service

Running SME as an ASP.NET Application

I am not going to explain in great detail how this works, as my friend, colleague, and mentor Omar Al Zabir describes this very elaborately in his article “Simulate a Windows Service using ASP.NET to run scheduled jobs”. To achieve this, we add a dummy page to the ASP.NET cache with a defined cache expiry time. When the cache expires, ASP.NET calls the CacheItemRemovedCallback function. In this function, we also call the DoWork() function, which dequeues the email messages. The HitPage() function is also called, which hits a dummy page and creates the HttpContext which is required by ASP.NET to register a cache item. This will again expire after the defined cache expiry time, and on expiry of the cache, ASP.NET calls the CacheItemRemovedCallback function…. And so on. So we create a constant loop here and keep the task running.

private void RegisterCacheEntry( )
{
  // Prevent duplicate key addition
  if (null != HttpContext.Current.Cache[DummyCacheItemKey])
      return;

  HttpContext.Current.Cache.Add(DummyCacheItemKey, 
              "Test", null, DateTime.MaxValue,
              TimeSpan.FromMinutes(1), 
              CacheItemPriority.NotRemovable,
              new CacheItemRemovedCallback(CacheItemRemovedCallback));
}

public void CacheItemRemovedCallback( string key, object value,
                                      CacheItemRemovedReason reason)
{
  System.Diagnostics.Debug.WriteLine("Cache item callback: " + 
                                     DateTime.Now.ToString());
  // Do the service works
  DoWork();
  // We need to register another cache
  // item which will expire again in one
  // minute. However, as this callback
  // occurs without any HttpContext, we do not
  // have access to HttpContext and thus
  // cannot access the Cache object. The
  // only way we can access HttpContext is
  // when a request is being processed which
  // means a webpage is hit. So, we need
  // to simulate a web page hit and then 
  // add the cache item.
  HitPage();
}

private void HitPage( )
{
  string siteprefix = ConfigurationManager.AppSettings.Get("SitePrefix");
  System.Net.WebClient client = new System.Net.WebClient();
  client.DownloadData(siteprefix + DummyPageUrl);
}

/// <summary>
/// Asynchronously do the 'service' works
/// </summary>
private void DoWork( )
{
  System.Diagnostics.Debug.WriteLine("Begin DoWork...");
  System.Diagnostics.Debug.WriteLine("Running as: " + 
         System.Security.Principal.WindowsIdentity.GetCurrent().Name);
  DequeueEmail();
  System.Diagnostics.Debug.WriteLine("End DoWork...");
}

private void DequeueEmail()
{
  //Calling the SME EmailDeQueue.Recieve 
  //to receive list the pending emails
  TList<EmailMessage> list = EmailDeQueue.Recieve(); 
  //Calling the SME EmailDispatch.Dispatch to finally deliver the emails
  EmailDispatch.Dispatch(list); 
}
Running SME as a Windows Service

I have also provided examples of running SME as a Windows Service. I have used a timer component to dequeue the mail after certain interval. The following piece of code does the trick.

JobRunning jRun = JobRunning.Instance();
private void timer_Elapsed(object source,System.Timers.ElapsedEventArgs e)
{
    if (!jRun.IsRunning)
    {
        try
        {
            jRun.IsRunning = true;
            DequeueEmail();
        }
        finally
        {
            jRun.IsRunning = false;
        }
    }
}

The JobRunning class implements the Singleton design pattern, and checks whether the previous job is still running. This protects against multiple DeQueueEmail operations starting at the same time.

How to Use Smart Mass Email in your own Application

Client Application Steps

The client needs to create an EmailMessage object and then can use the TemplateEmailProvider to template the message, and finally queue the message using EmailQueue.Send(templatedmessage).

The client application needs to add SmartMassEmail.Entities and the providers reference. Also, add the implemented provider for EmailQueueProvider, and TemplateEmailProvider in your solution. Check the app.config / web.config file of the demo application to understand about configuring the providers.

Server Application (EmailDeque and Dispatching) Steps

The EmailDequeue and Dispatch can run as a Windows Service or as an ASP.NET application, and will asynchronously dispatch the emails.

The Service needs a reference to SmartMassEmail.Entities providers and implemented providers for EmailDeQueueProvider, EmailDispatchProvider, and ProcessFailureProvider. Also check the app.config/web.config file of the demo application to understand configuring the providers.

B: So what you meant is, anyone can add the required DLLs into their solution and tweak the app.config/web.config, and the mass emailing feature is ready. They can also use a choice of Microsoft® SQL Server 2000 or Microsoft® SQL Server 2005, depending on what they have access to. Also, you have provided FileSystemEmailTemplateProvider for easy templating of messages.

A: That’s right. It is that easy to have mass emailing as part of your existing .NET application.

Conclusion

SME is a ready-to-use mass emailing feature that can be added to your existing .NET application. It has been architected keeping flexibility and extensibility in mind. SME ships with a couple of useful providers, but you can also write your own. Microsoft® SQL Server 2005 Service Broker is a very powerful technology for asynchronous processing, and SME takes full advantage of this. SME also can be used with existing SQL Server 2000 platforms, preserving compatibility with many previously implemented .NET 2.0 applications.

This can also be used as a reference application of how you can send and receive messages by communicating with the Service Broker from your .NET application. SME uses cutting edge .NET technologies available today such as: Enterprise Library, Microsoft® Provider Pattern, and Microsoft® SQL Server 2005 Service Broker.

Special thanks to Christopher Heale for proof reading this article.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Shahed.Khan
Web Developer
Australia Australia
Member
I have been awarded MVP (Visual C#) for year 2007, 2008, 2009. I am a Microsoft Certified Application Developer (C# .Net). I am born in Bangladesh and currently live in Melbourne, Australia. I am a co-founder and core developer of Pageflakes www.pageflakes.com and CEO at Simplexhub, a highly experienced software development company based in Melbourne Australia and Dhaka, Bangladesh. Simplexhub, is on its mission to build a smart virtual community in Bangladesh and recently launched beta www.realestatebazaar.com.bd an ASP.NET MVC application written in C#.NET.
My BLOG http://www.geekswithblogs.net/shahed
http://msmvps.com/blogs/shahed/Default.aspx.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5memberMohammed.Gafoor6 Mar '13 - 3:12 
Awesome..
QuestionThanks smtpserver.inmemberjsmtp.com4 Feb '13 - 21:30 
its really works
http://smtpserver.in
GeneralNice WorkmemberOba.Gerald9 Jan '13 - 15:32 
Nice Work.
GeneralMy vote of 5memberPatrick Harris12 Apr '12 - 9:28 
Just the fact that you wrote the article from the perspective of a chat session gets my vote of 5. Very creative Smile | :)
GeneralMy vote of 5memberAjit_Singh_vb30 Oct '10 - 0:22 
Superb article, provider model beautifully explained. Also, usage of service broker explains many of async processing much needed for decoupled application design. Excellent.
QuestionNot Send EmailmemberMember 353699911 Jan '09 - 20:57 
Hello,
 
I configured db, windows service, app.config and web.config but not come to my mailbox the email.
 
Help!!!
 
modified on Monday, January 12, 2009 3:06 AM

GeneralSQL 2005 DemomemberMember 35369995 Jan '09 - 2:23 
Hi,
 
Could you post a demo project for sql server 2005?
 
Thanks a lot!
QuestionPossiblity of using Oracle db?memberiamdudley29 Oct '08 - 14:55 
Nice work Shahed.
 
How easy would it be to hook this up to an Oracle (10g) database? Would it be similar to how you've used MSSQL2000? That is, without the service broker?
 
Cheers,
 
James.
GeneralSomething what i come acrossmemberzhangrh_88183 Sep '08 - 18:18 
(1). One reason for nothing is inserted into SMEPostQueue
Because Message Type define is as fallows:
CREATE MESSAGE TYPE [SMEMessageType] AUTHORIZATION
[dbo] VALIDATION = WELL_FORMED_XML
So if the message is not WELL_FORMED_XML,then anthing will be inserted into SMEPostQueue.
 
(2). For Sql2005EmailQueueProvider.Send Method
 
try to change from:
string sql =
string.Format (@"DECLARE @dialog_handle UNIQUEIDENTIFIER;
BEGIN DIALOG CONVERSATION @dialog_handle
FROM SERVICE [SMEPostingService]
TO SERVICE 'SMEService'
ON CONTRACT [SMEContract] ;
 
-- Send message on dialog conversation
SEND ON CONVERSATION @dialog_handle
MESSAGE TYPE [SMEMessageType]
('{0}') ;
 
End Conversation @dialog_handle
With cleanup", xml);
to
string sql =
string.Format(@"DECLARE @dialog_handle UNIQUEIDENTIFIER;
DECLARE @msg NVARCHAR(MAX);
BEGIN DIALOG CONVERSATION @dialog_handle
FROM SERVICE [SMEPostingService]
TO SERVICE 'SMEService'
ON CONTRACT [SMEContract];
 
SET @msg = '{0}';
 
-- Send message on dialog conversation
SEND ON CONVERSATION @dialog_handle
MESSAGE TYPE [SMEMessageType] (@msg);
 
End Conversation @dialog_handle With cleanup;", xml);
it seems that in some cases it Can help to pass the XML VALIDATE, but I can't tell why,it just do work.
 
(3) Maybe we should set the encoding when reading template file
The code snippet,
"new System.IO.StreamReader(string.Format(@"{0}{1}.txt", path, templateName))",
which is in FileSystemEmailTemplateProvider.cs,
We can change it according to the template file's encoding,for example:
"new System.IO.StreamReader(string.Format(@"{0}{1}.txt", path, templateName), System.Text.Encoding.Default))"
Generalwebapplication.sql2000 .the project does not support installationmemberMember 203703211 Aug '08 - 20:18 
I dowloaded the attachment project given . Gives me the mentioned error webapplication.sql2000 .the project does not support installation.
 
My m/c config is SQl 2000 + Win Xp+Vs 2005/2008/2003
 

Please Let me know hwt changes should i do to get it done for working
GeneralNice workmemberMember 175449026 Mar '08 - 5:33 
Hi Shahed,
 
Thanks for this article. It is very useful!!!
 
Prashant Ganesh,
AnswerSql Server 2000 Schemamembertoxaq9 Mar '08 - 13:32 
Oddly, after writing what seems like a lot of code to make Sql 2000 compatible, the author neglected the database schema. If you restore the database linked to in one of the comments in the forum and extract the SQL you get the following. Hopefully it will help someone.

if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[FK_EmailMessageDetail_EmailMessage]') and OBJECTPROPERTY(id, N'IsForeignKey') = 1)
ALTER TABLE [dbo].[EmailMessageDetail] DROP CONSTRAINT FK_EmailMessageDetail_EmailMessage
GO
 
if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[_EmailMessage_GetPendingEmailMessage]') and OBJECTPROPERTY(id, N'IsProcedure') = 1)
drop procedure [dbo].[_EmailMessage_GetPendingEmailMessage]
GO
 
if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[EmailMessage]') and OBJECTPROPERTY(id, N'IsUserTable') = 1)
drop table [dbo].[EmailMessage]
GO
 
if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[EmailMessageDetail]') and OBJECTPROPERTY(id, N'IsUserTable') = 1)
drop table [dbo].[EmailMessageDetail]
GO
 
CREATE TABLE [dbo].[EmailMessage] (
[ID] uniqueidentifier ROWGUIDCOL NOT NULL ,
[ChangeStamp] [datetime] NOT NULL ,
[Priority] [int] NOT NULL ,
[Status] [int] NOT NULL ,
[NumberOfRetry] [int] NOT NULL ,
[RetryTime] [datetime] NOT NULL ,
[MaximumRetry] [int] NOT NULL ,
[ExpiryDatetime] [datetime] NOT NULL ,
[ArrivedDateTime] [datetime] NOT NULL ,
[SenderInfo] [varchar] (1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[EmailTo] [varchar] (1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[EmailFrom] [varchar] (1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[EmailSubject] [varchar] (1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[EmailBody] [text] COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[EmailCC] [varchar] (1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[EmailBCC] [varchar] (1000) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[IsHtml] [bit] NOT NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
 
CREATE TABLE [dbo].[EmailMessageDetail] (
[ID] [uniqueidentifier] NOT NULL ,
[ChangeStamp] [datetime] NOT NULL ,
[IsBinary] [int] NOT NULL ,
[Name] [varchar] (250) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[BinaryData] [image] NOT NULL ,
[StringData] [text] COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
[EmailMessageID] [uniqueidentifier] NOT NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
 
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_NULLS OFF
GO
 
CREATE PROCEDURE dbo._EmailMessage_GetPendingEmailMessage
 
AS
SELECT top 5
[ID],
[ChangeStamp],
[Priority],
[Status],
[NumberOfRetry],
[RetryTime],
[MaximumRetry],
[ExpiryDatetime],
[ArrivedDateTime],
[SenderInfo],
[EmailTo],
[EmailFrom],
[EmailSubject],
[EmailBody],
[EmailCC],
[EmailBCC],
[IsHtml]
FROM
dbo.[EmailMessage]
where NumberOfRetry < MaximumRetry and RetryTime < getdate() and Status = 0
order by Priority,RetryTime

Select @@ROWCOUNT
GO
SET QUOTED_IDENTIFIER OFF
GO
SET ANSI_NULLS ON
GO

GeneralRe: Sql Server 2000 SchemamemberMember 437239410 Aug '09 - 6:50 
Excellent work. Helped me a lot.
GeneralLet me Know How i Attatch any file with an Email Messagememberirshad ali1 Feb '08 - 22:25 
Assalam -ul- Alaikum !
 
Nice man you have did wonderful work. I was assign a task to me in the office for bulk mailing. But i was worried that how would i implement it. My all mail was treating as a Spam mail. But when i found your article and source code. i used it and it did fine. thanks one again.
 
dear in implementation i am getting two problems
 
1)- Let me Know How i Attach any file with an Email Message
2)- how i confirm via your code that whether mail has been sent or not means mail acknowledgment.
 
Thanks dear you are a wonderful developer. Please reply this message.
 
Allah Hafiz
 
hello join me and give me some useful information
 
allah hafiz
irshad

Questionhow to use the web-service version ?memberamrlafi6 Sep '07 - 5:45 
Hi ,
how to use the web-service version ??
 
Thanks !
GeneralWhy use dummy ASP.NET page?..memberDavid Mumladze13 Aug '07 - 5:09 
You could also use **HttpRuntime.Cache["YourKey"]** instead of HttpContext.Current.Cache["YourKey"] and achieve the same with slightly better performance...
 
Didn't your mentor told you that?.. Laugh | :laugh: Poke tongue | ;-P
 
Please re-factor your code!
Questionhow can send multiple messages...??memberdrfrrvfrcv3 Jul '07 - 0:18 
thanks for this great article..it have a lots of nice things working together.i have 2 problems though to get it working:-
 
1)i have created database For SQL Server 2005 but couldnt able to send mails..my executenonquery method of Sql2005EmailQueueProvider.cs class is returning -1 so is it problem..list variable passed in dispatch method shows count = 0
 
2)When i tried to implement it in SQL Server 2000, i was successful but can anyone tell me how can i send multiple messages through aspx page.
Please reply as soon as possible as i am working on thios for soetime but couldnt complete it..
 
Thanks in advance
 

mayank gupta

Generalscalabilitymembersharma sanjeev16 May '07 - 2:41 
One more question.
I tried sending mass emails but it seems to be slow (nearly 700 mails in an hour), how many mails can i send in a day using this framework ?? I am trying to build a mass emailing system which is capable of sending around a million mails a day.
can you please suggest what hardware configuration will be suitable for such a system ? or any thin else i need to worry about?
 
Need your valuable suggestions on this.
 

QuestionSME with sql 2005 expressmembersharma sanjeev23 Apr '07 - 17:58 
I use visual studio 2005 with sql 2005 express. Can i use Mass Emailing Functionality with sql 2005 express ??
Any workaround for this ?? Confused | :confused:
 

AnswerRe: SME with sql 2005 expressmemberShahed.Khan25 Apr '07 - 14:54 
I have not tested with sql2005 express. But should work fine... just point the SMESql2000 providers to your sql2005 express database by changing the connectionstring. And you need to create all the tables related to SME in your sql2000 express database. The DataAccessLayer of the Sql2000 providers use .NetTiers so it should be fine I think.
 

 

 
Shahed Khan (MVP Visual C#)

GeneralRe: SME with sql 2005 expressmembersharma sanjeev14 May '07 - 20:32 
1. I have to use SMESql2000 providers or SMESql2005 providers ? for using SME with sql 2005 express
 

2. Its all working fine when i use vs.net 2005 and sql server 2005
One more question.
I tried sending mass emails but it seems to be slow (nearly 700 mails in an hour), how many mails can i send in a day using this framework ?? I am trying to build a mass emailing system which is capable of sending around a million mails a day.
can you please suggest what hardware configuration will be suitable for such a system ? or any thin else i need to worry about?
 
Need your valuable suggestions on this.
 
with best regards
sanjeev sharma
India)
GeneralCharacter Encoding [modified]memberSatish Kumar B.G2 Apr '07 - 0:31 
Hi!
 
While sending email message with special chars, these characters go missing when the email is recieved, any suggestions. Wehave used the default utf-8 encoding as in the SME, no changes anywhere. Has it got to do with the Sql2005EmailQueueProvider send method's xml serialization, or with Sql2005EmailDeQueueProvider ProcessMessage method's Encoding.Unicode.GetBytes(xml);
or the SMTPDotnet2EmailDispatchProvider Dispatch method's message.BodyEncoding ?
 
Regards
Satish
 

 
-- modified at 11:38 Monday 9th July, 2007
GeneralRe: Character Encodingmemberrohant531 Jul '07 - 15:08 
Hi Satish,
 
Were you able to get a solution to this.
 
Regards,
 
Rohit
GeneralRe: Character EncodingmemberSatish Kumar B.G14 Aug '07 - 22:28 
No, it seems to live on. Tried removing the well-formed XML validation, but that didn't help. The char's go missing when they are enqueued, no error reported. Had to do a (trivial) custom html-encoding (find & replace char's with some custom html encoding) before queuing and after de-queuing. Will have to look at this issue once again when i find time.
 
Regrds
Satish
GeneralProblems (and solutions) to using SME with Service BrokermemberVassil Tochev15 Mar '07 - 19:40 
I did a quick implementation of the SME. I had some issues/problems which I addressed with quick fixes that might not be 100% correct or very elegant but I hope that by sharing them somebody might find them useful.
 
1) I was not able to queue my messages to the SQL 2005 Service Broker.
At first it was as if the SEND ON CONVERSATION was doing nothing.
I discovered that because of the With cleanup instruction at the end of the query, nothing was stored in the queue and I couldn't see any errors. After removing it I was able to see my messages in sys.transmission_queue and their errors, I discovered that my messages were flagged with:
The session keys for this conversation could not be created or accessed. The database master key is required for this operation
To solve this I had to run:
create master key encryption by password = 'mypassword'
 
2) Now my messages were getting in and I could see them by running the following query: select * from dbo.SMEPostQueue. However I started having errors when trying to deserialize my database messages.
- In ProcessMessage( SqlDataReader reader ) I had to change this byte[] bytes = Encoding.Unicode.GetBytes(xml); to byte[] bytes = Encoding.UTF8.GetBytes( xml );. How did this even work in the first place? In the serializing of the messages UTF8 is used in ConvertByteArrayToString( byte[] byteArray ).
 
- In CreateReceiveCommand() we have the query to dequeue messages from database. The provided query did not work for me. Once out of the database my string that was supposed to contain the original xml was not readable at all and the deserializer did not like it. The following change to the query fixed it for me. It was CONVERT( NVARCHAR(max), message_body ) as mgb, I changed it to CONVERT( XML, message_body ) as mgb
 
- Finally and this one is probably only related to my code. I had problems queuing my messages. The thing is I am not using the EmailMessage class for my messages, I use my own one. Nevertheless, I use the same code as in Send(EmailMessage message) where we format our xml in the sql string that is used to queue the message. I was having problem when my xml was containing the ' character. Maybe using the MailMessage class and then serializing it won't cause the xml to contain this character. Also I think that it would be better to use an sqlparamater here instead of using format but I left the code as is and just before formatting the sql with the xml string I added a xml = xml.Replace( "'", "''" ); and that worked. This of course was causing the sql query to end prematurely and cause a weird tsql error.
 
3) After all the changes, the SME code seems to work fine until I started testing the "mass" mailer with a test run of 100 messages. My implementation of the SME is done like this:
- Web site for creating the email list and starting the process.
- Web service that checks if any messages are in the queue and then dequeues them and sends the emails.
- A windows service to simply tell the web service to do it's work (on a timer of course)
 
The web site and the web service are hosted by my hosting company because I want to use their smtp server. Plus they dont allow remote use of their smtp server.
The windows service runs on a personal server. I did not want to use the asp.net service for many reasons. Although it does sound like a very interesting approach and the related articles were very insightful.
 
So all this to tell you that my current problem is that when I test run my setup with 100 emails, it stops sending them after 20. Actually the number has nothing to do with the problem, I was able to find out that I get a Thread was being aborted. exception in the Dispatch method which I remind you is sitting in my web service. I presume that this is caused by an execution timeout of the web service. I haven't solved this but I think that I'm going to rewrite some of the code in the Recieve() method. I think that the one call to Recieve(), in my case, dequeues all the 100 messages (btw I checked and my queue has all the 100 msgs in there) and then passes the list to the Dispatch method which takes too long to execute.
I think I will try limiting this to 10 emails dequeued at once and then 10 emails dispatched.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130516.1 | Last Updated 8 Sep 2006
Article Copyright 2006 by Shahed.Khan
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid