Standard
Mold is interested in creating a Notification service. They have a
number of different systems that already notify people about IT system
failures and outages, but this legacy architecture requires that their
SMS sending software support four different SMS vendors, and has
therefore become complex and expensive to maintain (Figure 1).
Furthermore, they want to expand the use of their notifications, as follows:
they would like managers to be notified when inventory is running low
customers may need to be notified when specific types of new inventory arrive at the warehouse
the billing department may want to be able to notify customers as soon as payments are considered overdue
The team of architects
responsible for designing these new notification functions is still
interested in issuing notifications with SMS. Even though it is more
costly and restrictive than issuing e-mail notification messages,
business analysts have determined that SMS notifications are more likely
to be received and read sooner.
The design of the service begins with the application of the Standardized Service Contract principle by following a contract-first approach. Architects design a NotificationMessage,
a NotificationStatus type and a WSDL definition with a NotifyBySms
operation and then proceed to use the WCF.blue tool to generate
implementation code from the WSDL definition.
A single SMS vendor was chosen
for use by the new service. This vendor provides a proprietary .NET API
that the service will need to access. A sample code fragment used to
interact with this API is shown here:
Example 1.
TransmitterClientProxy proxy = new TransmitterClientProxy(); int smsid = proxy.SendShortTextMessage ( 12309723, //customer id "T0pSecre7", //password 1, //priority "You recieved this SMS from Standard Mold", //message of SMS 7002348234781, //receiver null, //delay hours true, //delivery report wanted "https://standardmold.com/delivery_report/{messageId} /{state}" // delivery report address );
|
Some of the parameters
for this API are populated using configuration and message-specific
parts of the SMS message. The actual text message and the receiving
phone number is populated using the incoming request message.
The Notification
service’s NotifyBySms operation and related NotifyBySmsRequest message
are designed independently of this API. As a result, contract-to-logic
coupling is avoided, allowing Standard Mold architects to change SMS
vendors in the future, if required.
The Notification service needs to
be able to perform its processing in an asynchronous fashion.
Therefore, architects determine that it would be sufficient for the
service to issue one-way notification messages without the need for any
acknowledgement of receipt by service consumers.
Specifically, the NotifyBySms operation is designed to works as follows:
1. | When
the notification request reaches the Notification service, it saves
some information about the notification, gives it a NotificationId
value, and puts the request in a queue.
|
2. | The
Notification service sends a response to the service consumer
containing the newly generated NotificationId. As a result, the consumer
is able to use this ID value when it wants to poll for the status of
the message.
|
3. | A
transaction wraps this functionality in order to ensure the integrity
of the database and the queue insertions. On the other side of the queue
a Windows service is positioned to listen, send the SMS, and update the
database accordingly. A transaction is also used here to ensure these
steps are carried out together.
|
The technologies chosen for this service architecture are:
The service interface
(called the Notification_receiver) is implemented using WCF. A
transaction scope is defined to keep the SQL Server database
(Notification_db) and MSMQ queue (Notification_queue) processing
together.
To make interacting with MSMQ
easier, they wrap the queue in a WCF interface. Notification_sender is
the Windows service on the other end of the queue that uses a
transaction scope to read a message from MSMQ, send a message using the
API (not part of the transaction), and save the status of the message in
the database. This architecture is illustrated in Figure 2.
This design allows the
different parts of the service to be scaled independently, depending on
future needs and requirements. The sender can be scaled out if necessary
without touching the receiver, and the receiver part can be scaled out
without touching the sender.
The GetNotificationStatus
operation was created in order to allow consumers to find out more about
the status of the notification (whether or not the SMS was sent or even
opened).
Using the previously created NotificationId,
service consumers can get the same kind of information as the
NotifyBySms operation returns (the status of the notification). However,
when calling the GetNotificationStatus operation, the consumer does get
a return value signifying that the SMS was sent or opened, in case the
recipient has already opened the SMS using a cell phone. This
functionality was made possible by the SMS vendor and allows for a
delivery report address to be added to the call using their proprietary
API.
The WCF interface of the service (receiver) looks like this:
Example 2.
[ServiceContract (Name = "Notification", Namespace = "http://schemas.standardmold.com/ enterprise/services/v1") ]
public interface INotificationReceiver { [OperationContract] NotificationResponse NotifyBySms (NotificationMessage message); [OperationContract] NotificationResponse GetNotificationStatus (Guid NotificationId); }
|
The implementation of the
GetNotificationStatus method is relatively straightforward. It basically
gets the status of a specific NotificationId from the database.
The NotifyBySms method is also simple, in that it extracts the MessageId from the incoming message headers. The data type of the MessageId is UniqueId. From this field, a GUID is extracted and passed along with the notification message to the ProcessMessage method:
Example 3.
public NotificationResponse NotifyBySms (NotificationMessage message) { UniqueId messageId = OperationContext.Current. IncomingMessageHeaders.MessageId; Guid messageGuid = Guid.Empty; if (messageId.TryGetGuid(out messageGuid)) { return this.ProcessMessage(message, messageGuid); } else { return new NotificationResponse { MessageStatus = Status.Failed }; } }
|
The ProcessMessage method first uses the DataAccess
class to check if the notification was processed before. If not, it
attempts to save the message to the database and put it in the queue.
This is done inside a transaction scope, so either both or neither of
these operations succeed. In case of a failure, the status of the
notification in the response message is set to “failed.”
As previously mentioned,
the notification may have been processed before a response is
constructed using the current status of this notification that was
returned from the database. In this case, the status can be saved (if
the notification was not yet sent), or it can be set to another status
value, such as “sent” (if the notification was sent to the consumer).
The following example shows
how the message is saved to the database (with status “saved”) and
placed in the queue. Possible responses include the communication of
success with the NotificationId value or a response of failure in case of an exception condition.
Example 4.
private NotificationResponse ProcessMessage (NotificationMessage message, Guid messageGuid) { NotificationResponse response = null; Guid notificationId = Guid.Empty; var notificationData = DataAccess.GetNotificationData(messageGuid); if (notificationData.NotificationNotProcessed) { try { using (var scope = new TransactionScope()) { notificationId = DataAccess.SaveMessage (message, messageGuid); MsMqAction.SendToMsmq(message, notificationId); scope.Complete(); } response = new NotificationResponse { MessageStatus = Status.Saved, NotificationId = notificationId }; } catch { response = new NotificationResponse { MessageStatus = Status.Failed }; } } else { response = new NotificationResponse { MessageStatus = notificationData.MessageStatus, NotificationId = notificationData.NotificationId }; } return response; }
|
The DataAccess class checks the messageGuid. If it was set by the client (that is, the messageGuid is not Guid.Empty), it attempts to find it in the database using LINQ to SQL. If it was found in the database, the NotificationId and NotificationStatus properties are set in a NotificationData object:
Example 5.
public NotificationData GetNotificationData(Guid messageGuid) { using (NotifyDBDataContext db = new NotifyDBDataContext()) { var notificationEntry = (from notification in db.NotificationDataEntries where notification.MessageId == messageGuid select notification).SingleOrDefault(); if (notificationEntry != null) { return new NotificationData { NotificationId = notificationEntry.NotificationId, NotificationStatus = notificationEntry.NotificationStatus }; } } return new NotificationData(); }
|
The NotificationData
class is also important in this example as it helps the ProcessMessage
method determine if it should go on to process this notification, or
simply return status information about it. If the notification was
already processed, the NotificationData instance that the GetNotificationData method returns will contain values for NotificationId and NotificationStatus.
Example 6.
public class NotificationData { public Guid NotificationId { get; set; } public Status NotificationStatus { get; set; } /// <summary> /// if NotificationId was not set this /// notification was not processed /// </summary> public bool NotificationNotProcessed { get { return NotificationId.Equals(Guid.Empty); } } }
|
The
SendToMsmq method is responsible for putting the notification inside
the MSMQ queue by creating an MSMQ message and then using WCF to put the
message into the queue:
Example 7.
public void SendToMsmq (NotificationMessage message, Guid notificationId) { IdentifiedNotification notification = new IdentifiedNotification { NotificationId = notificationId, NotificationMessage = message }; var msmqMessage = new MsmqMessage <IdentifiedNotification>(notification); msmqMessage.Priority = MessagePriority.Highest; var client = new MsmqNotificationClient ("NotificationEndpoint"); client.PutNotification(msmqMessage); client.Close(); }
|
Standard Mold architects build a
WCF program that communicates with MSMQ queue by creating an interface
and a client implementation along with some simple configuration:
Example 8.
[ServiceContract] public interface IMsmqNotificationQueue { [OperationContract(IsOneWay = true, Action = "*")] void PutNotification(MsmqMessage <IdentifiedNotification> msg); } public partial class MsmqNotificationClient : ClientBase<IMsmqNotificationQueue>, IMsmqNotificationQueue { public MsmqNotificationClient() { } public MsmqNotificationClient(string configurationName) : base(configurationName) { } public MsmqNotificationClient (Binding binding, EndpointAddress address) : base(binding, address) { } public void PutNotification (MsmqMessage<IdentifiedNotification> message) { base.Channel.PutNotification(message); } }
|
The significant configuration is as follows:
Example 9.
<client> <endpoint address="msmq.formatname: DIRECT=OS:.\private$\NotificationSMS" binding="msmqIntegrationBinding" contract="MsMqIntegration.IMsmqNotificationQueue" bindingConfiguration="NotificationEndpointBinding" name="NotificationEndpoint" /> </client>
|
Developers choose to use the msmqIntegrationBinding because netMsmqBinding would make it more complicated to deserialize messages from the queue when implementing the sender.
The sender is implemented as a Windows service using the MSMQ API. It sets up a listener for the queue in the OnStar method.
Example 10.
protected override void OnStart(string[] args) { string queueName = ConfigurationManager. AppSettings["queueName"]; if (!MessageQueue.Exists(queueName)) MessageQueue.Create(queueName, true); MessageQueue queue = new MessageQueue(queueName); queue.ReceiveCompleted += new ReceiveCompletedEventHandler (MessageReceiveCompleted); queue.BeginReceive(); }
|
Finally, the MessageReceive
method sends the SMS using the legacy API (currently the API provided by
the SMS vendor). It also removes the message from the queue and updates
the notification status in the database.
Example 11.
static void MessageReceiveCompleted(object sender, ReceiveCompletedEventArgs asyncResult) { var queue = (MessageQueue)sender; using (var scope = new TransactionScope()) { var message = queue.EndReceive(asyncResult.AsyncResult); message.Formatter = new XmlMessageFormatter(new Type[] {typeof(IdentifiedNotification)}); var identifiedNotification = (IdentifiedNotification)message.Body; SmsSender.Send(identifiedNotification. NotificationMessage); DataAccess.UpdateStatus(identifiedNotification. NotificationId, Status.Sent); scope.Complete(); } queue.BeginReceive(); }
|
The Notification service is ready for deployment. In support of applying the Service Discoverability
principle, the Standard Mold team decides to publish supporting
documentation alongside the service contract and SLA. This content is
intended for project teams building service consumers and provides
guidelines on how to effectively use the Notification service, along
with sample code that demonstrates how to call the service using a WCF
service consumer:
Example 12.
NotificationResponse response = null; System.Xml.UniqueId messageId = new System.Xml.UniqueId(Guid.NewGuid()); WcfClient.Using(new NotificationClient(), client => { using (new OperationContextScope(client.InnerChannel)) { OperationContext.Current. OutgoingMessageHeaders.MessageId = messageId; response = client.NotifyBySms(new NotificationMessage { Recipient = "+13263827", Message = "Please note that something happened" }); } });
|