Sample: Creating a Notifications System with iKnode.

In this sample tutorial, we are going to build a production ready Notifications System with iKnode and your browser.

The first thing we need to do is define a clear set of requirements we want to support:

  1. The system must be able to receive a notification message and send it to the selected recipient.
  2. The system must not loose any notification messages.
  3. The system must be able to wait and send the messages in case there is a problem with the email system itself.
  4. The notification send process must be very fast for the client application.

To cover the first requirement, we could just have one iKnode Application called ‘Notifications’:

The code for that application would be a simple emailer that takes a message and sends it:

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
using System;
using System.Net;
using System.Text;
using System.Net.Mail;
using iKnode.Applications;
using iKnode.Applications.Data;

namespace Applications
{
    /// <summary>
    /// Defines the Main entry point of the Notifications application.
    /// </summary>
    [Application]
    public class Notifications
    {
        private const string Server = "myServer";
        private const int Port = 587;
        private const bool UseSsl = true;
        private const string UserName = "notifications@myemail.com";
        private const string Password = "myPassword";

        /// <summary>
        /// Sends a notifications message through an SMTP Server.
        /// </summary>
        /// <returns>True if the message was sent, false otherwise.</returns>
        public bool Send(string recipient, string subject, string body, bool isBodyHtml)
        {
            using (MailMessage emailMessage = CreateMailMessage(UserName, recipient, subject, body)) {
                SmtpClient smtpClient = new SmtpClient(Server, Port);
                smtpClient.UseDefaultCredentials = false;
                smtpClient.EnableSsl = UseSsl;
                smtpClient.Credentials = new NetworkCredential(UserName, Password);
                smtpClient.Send(emailMessage);
            }

            return true;
        }

        /// <summary>
        /// Creates an email message from a notification message.
        /// </summary>
        /// <returns>Mail Message.</returns>
        private static MailMessage CreateMailMessage(string sender, string recipient, string subject, string body, bool isBodyHtml = false)
        {
            MailMessage emailmsg = new MailMessage();

            emailmsg.To.Add(new MailAddress(recipient));
            emailmsg.From = new MailAddress(sender);
            emailmsg.Body = System.Web.HttpUtility.HtmlDecode(body);
            emailmsg.Subject = subject;
            emailmsg.IsBodyHtml = isBodyHtml;

            emailmsg.BodyEncoding = Encoding.UTF8;

            return emailmsg;
        }
    }
}

This is a very simple application that receives a recipient, subject, body and a flag if the message’s body is HTML. You can create and publish this application in iKnode; and then test it in the console with the following command:

Code
1
Notifications:Send --recipient=my@email.com --subject=Test --body=TestNotification --isBodyHtml=false

You should get a True result and receive an email for the address set in the recipient field.

This application fulfills the first requirement, and it works fine, but it doesn’t support the other 3 requirements.

For example, if the email server is down, the Notifications application will return false, the message will be lost completely. In order to send the message, the client needs to send it again. This breaks the requirement #2: The system must not loose any notification messages. This also breaks requirement #3, since the message will never be sent, unless the clients creates the message and sends it again.

Another problem with this design is that if the email server is saturated or very far from the iKnode server, the Notifications Application will take a long time to process.

The problem with this application, in a nutshell, is that it puts all the burden of the Notification Sending process in the client. If the email server is down, then the user needs to keep track of its own notifications so that it can try again. Additionally, if the email server is down, the system puts the burden on the client to recreate and try again. And finally if the email server is too far or there are connectivity problems, the burden of the connection time is put on the client again, making the client slow as well.

Another thing to think about, is that if there ever is a requirement to send notifications through multiple channels not just email(e.g. Twitter, SMS, or Facebook notifications), this Application would need to be redesign in a big way.

After understanding the problems that plague this design, let’s revise it to meet the rest of the requirements:

What we need to do in order to support the other 3 requirements is to separate the ‘Email Sending’ functionality from the ‘Notifications Send Request’. This separation will create a buffer between the Notifications Request Application and the Email Application.

The Notifications Application then receives the request, stores it in the database and returns to the client, which happens to be very fast. Then an scheduled Emailer Application, runs every 20 seconds to pick up any unsent emails and sends them. Once the email has been sent successfully, the Emailer will removes it from the database, and continues with the next.

If, for ever reason, the email is not sent, the Emailer sends the other emails in the database without removing the failed message. The Emailer will then try again the next time.Any email that was not successfully sent, remains in the database until it is possible to so.

The new design has two application classes and one Collection called Notifications:

The first thing we need to do is create the ‘Notifications’ collection in iKnode by just using the UI. The structure should like the following:

Once the Collection is created, let’s update the Notifications Application to follow the diagram described before:

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System;
using iKnode.Applications;
using iKnode.Applications.Data;

namespace Applications
{
    /// <summary>
    /// Defines the Notifications Application.
    /// </summary>
    [Application]
    public class Notifications
    {
        /// <summary>
        /// Executing User Identifier.
        /// </summary>
        private static readonly Guid UserId = new Guid(""); // Put your userId here.

        /// <summary>
        /// Sends a notifications message.
        /// </summary>
        /// <returns>Send Result.</returns>
        public string Send(string recipient, string subject, string body, bool isBodyHtml)
        {
            Document doc = new Document("Notifications");
            doc.Fields.Add("recipient", recipient);
            doc.Fields.Add("subject", subject);
            doc.Fields.Add("body", body);
            doc.Fields.Add("isBodyHtml", isBodyHtml.ToString());

            DocumentRepository repository = new DocumentRepository(UserId);

            string docId = repository.Save(doc);

            string result = String.Format("Notification could not be placed in the queue for {0}", recipient);
            if(!String.IsNullOrEmpty(docId)) {
                result = String.Format("The Notifications has been successfully added to the queue for {0} with confirmation id {1}", recipient, docId);
            }

            return result;
        }
    }
}

Once the application is published, test it with the same command we used before:

Code
1
Notifications:Send --recipient=my@email.com --subject=Test --body=TestNotification --isBodyHtml=false

Now it is time to move to the Emailer; the code for the emailer is displayed below, which is pretty much the same Notifications Application that we had initially with the exception of a new method called RunAsScheduled.

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
using System;
using System.Net;
using System.Text;
using System.Net.Mail;
using iKnode.Applications;
using iKnode.Applications.Data;

namespace Applications
{
    /// <summary>
    /// Defines the Main entry point of the Emailer application.
    /// </summary>
    [Application]
    public class Emailer
    {
        private const string Server = "myServer";
        private const int Port = 587;
        private const bool UseSsl = true;
        private const string UserName = "notifications@myEmail";
        private const string Password = "myPassword";
        private const int BatchSize = 10;

        /// <summary>
        /// Executing User Identifier.
        /// </summary>
        private static readonly Guid UserId = new Guid(""); // Put your userId here.

        /// <summary>
        /// Run the emailer as an scheduled task using the "Notifications" collections.
        /// </summary>
        public string RunAsScheduled()
        {
            string collectionName = "Notifications";

            DocumentRepository repository = new DocumentRepository(UserId);

            string query = "{}";
            PagedList<Document> notificationsToSend = repository.QueryCollection(collectionName, query, "", "ASC", 0, BatchSize);

            if(notificationsToSend != null && notificationsToSend.List.Count > 0) {
                foreach(Document doc in notificationsToSend.List) {
                    if(this.Send(doc.Fields["recipient"], doc.Fields["subject"], doc.Fields["body"], ConvertToBool(doc.Fields["isBodyHtml"]))) {
                        repository.Delete(doc);
                    }
                }
            }

            return string.Format("Notifications Sent {0}", notificationsToSend.List.Count);
        }

        /// <summary>
        /// Sends a message through an SMTP Server.
        /// </summary>
        /// <returns>True if the message was sent, false otherwise.</returns>
        public bool Send(string recipient, string subject, string body, bool isBodyHtml)
        {
            using (MailMessage emailMessage = CreateMailMessage(UserName, recipient, subject, body)) {
                SmtpClient smtpClient = new SmtpClient(Server, Port);
                smtpClient.UseDefaultCredentials = false;
                smtpClient.EnableSsl = UseSsl;
                smtpClient.Credentials = new NetworkCredential(UserName, Password);
                smtpClient.Send(emailMessage);
            }

            return true;
        }

        /// <summary>
        /// Creates an email message from a notification message.
        /// </summary>
        /// <returns>Mail Message.</returns>
        private static MailMessage CreateMailMessage(string sender, string recipient, string subject, string body, bool isBodyHtml = false)
        {
            MailMessage emailmsg = new MailMessage();

            emailmsg.To.Add(new MailAddress(recipient));
            emailmsg.From = new MailAddress(sender);
            emailmsg.Body = System.Web.HttpUtility.HtmlDecode(body);
            emailmsg.Subject = subject;
            emailmsg.IsBodyHtml = isBodyHtml;

            emailmsg.BodyEncoding = Encoding.UTF8;

            return emailmsg;
        }

        /// <summary>
        /// Converts the selected string to Boolean.
        /// </summary>
        private static bool ConvertToBool(string val)
        {
            return (val != null && val.Trim().ToLower().Equals("true"));
        }
    }
}

The RunAsScheduled method is a convenience method running as an scheduled application. What it does is to get a batch of 10 notifications every time time it runs and tries to send them via email. If it is sent successfully then it deletes it from the collection.

You can test this method in the console by running the following command:

Code
1
Emailer:RunAsScheduled

You may notice that the method Send is still there. The reason is that the Emailer is really an iKnode Application. Even if an instance of it is being ran in an schedule, nothing stops you from calling the emailer directly like this:

Code
1
Emailer:Send --recipient=my@email.com --subject=Test --body=TestNotification --isBodyHtml=false

The last thing to do is configure the Scheduled Job to run every 20 seconds executing the following Command:

Code
1
The last thing to do is configure the Scheduled Job to run every 20 seconds executing the following Command:

This can be done by using the Job Scheduling feature of iKnode:

The Command Name item can be added by using the search tool:

The trigger is creates using cron format, so “0/20 * * * * ?” means every 20 seconds. To construct your cron triggers just use CronMaker

Save your job, and go to the Job Scheduler List and make sure your Job is running normal:

You can monitor your Scheduled Job by using the Execution Log:

And voila! We have a robust Notifications System.

We have created a forum specially to discuss this sample here, if you have any questions please post them in the forum so that everyone can discuss.

[Download Source Code]