Introduction
Starting with Content Studio version 5.2, a powerful subscription service is included with the product. The service is used to send messages to subscribers when a document is approved or when a document is forced to be sent out. By default, the source of the message to be sent out is a Content Studio document and the subscribers are stored in the new Subscriber repository in Content Studio. With the current implementation, the only supported messagetype is mail messages in Html format generated from the document that triggered the sending operation.
Sometimes the default implementation is not sufficient or does not meet the requirements from your customers. For example you might want to ...
- get the subscribers from the Active Directory or a custom database.
- apply special formatting to the messages based on each individual subscriber.
- filter out the receiver based on som data in the document (i.e. an Ept-field)
- provide detailed logging to a location of your choice
- insert extended information in the message based on some personal data of the subscriber
As you can see, the list can get as long as you like. This article describes how to write a subscription handler from scratch or, preferably, a subscription handler that inherits one of the base classes provided in Content Studio. These base classes makes the task of creating a subscription handler far more easy that writing the handler from scratch.
Subscription Services under the hood
When a category is set up for subscription, its newly created and published documents are ready to be sent out to subscribers. For more information on how to configure and set up a category for subscription see the Subscription Services section in the Technical Overview documentation. When a document that is subject to subscription is published for the first time the event OnDocumentPublish is detected and triggered by DEPRO (Dynamic Event Processor, a part of the Content Studio Service Manager). DEPRO then queries Content Studio to see if the document's category happends to be enabled for subscription. When this is true DEPRO extracts all the relevant subscription data from the category subscription definition and creates a new OnDocumentSubscription Event Actions scheduled to be executed at the next point in time defined in the schedule set up on the category subscription definition. When this point in time has occured, Content Studio Service Manager finds the job and eventually calls the specified subscription event handler that performs the actual distribution of the message to its subscribers.
As mentioned above, the implementation provided with Content Studio creates a mail message based on the content of the document that triggered the subscription event and can only send messages to subscribers in the built in repository. However, this implementation is intended to satisfy the need for a vast majority of web sites and developers but in rare cases other needs exist. In this case you must create and provide your own event handler implementation to Content Studio Service Manager.
Extending the built in handlers
As mentioned earlier in this article, a Content Studio Subscription handler is a
specialized form of an asynchronous event handler that gets executed in the
background by the Service Manager whenever a subscription job has been
submitted. Every Content Studio asynchronous event handler implements the
This library can be found in the CSSubscriptionEventHandler assembly that gets installed in
the program files folder of Content Studio (by default C:\Program files\Teknikhuset\Content Studio 5\CSServer).
Launch Visual Studio 2005 or later and create a new class library project and set a reference to both the CSSubscriptionEventHandler.dll and CS5Interfaces.dll assemblies. CS5Interfaces.dll is installed in the GAC so you should be able to find it there or among the Content Studio program files.
Depending on the functionality to implement you can choose to derive from one of the three base classes provided by this library. These classes are:
SubscriptionEventHandlerBase - This is the base class that all subscription event handler derive from.
It implements the
ICSAsyncEventHandler and IDisposable interfaces. This class parses and validates the passed in data from Content Studio and defines a number of methods that you should or must override. MailSubscriptionHandler - This class inherits from
SubscriptionEventHandlerBase and adds mail sending capabilities. This class implements the CreateMessage method where it creates an Html mail message based on the Content Studio document that triggered the event. CSMailSubscriptionHandler -
Provides a working implementation of a Content Studio Subscription Mail message event handler.
This class generates inherits the
MailSubscriptionHandler and adds functionality that to send out mails to subscribers stored Content Studio.CSMailSubscriptionHandler can handle both the OnDocumentSubscription and adds functionality that sends out mails to subscribers stored Content Studio.CSMailSubscriptionHandler data passed in to the job queue in Content Studio that and OnTestDocumentSubscription events. In the former case all subscribers that receives mails are read from the Content Studio Subscription definition repository on the actual category. In the latter case a singel subscriber is read from the data passed in to the job queue in Content Studio that defined the event handled. - This is the implementation that Content Studio uses by default when i sends out Subscription mail messages.
When you would like to... | Class to extend | Comments |
---|---|---|
Send messages that are not email |
|
|
Send mails to a subscribers not stored in Content Studio |
|
By default the mail message is constructed from the passed in document and the mail
message has both a Html and text part. Override the
|
Send mails to subscribers in Content Studio, but you need full control over how the message is created. |
|
Override the
|
Send mails to subscribers in Content Studio, but you need full control over to whom the message is sent. |
|
Override the
|
During the life time of a handler there are a number of metods that gets called and events that fires, all which can be used by a developer to provide her own functionality.
ValidateEvent() - Derived classes override this method to specify which type of event can be handled by the handler.
CreateLogger() -
Creates the log specified by the
ISysLogWriter member of the extended properties passed in. Normally there is no reason to override this method. Initialized - Subscribe to this event if you like to do your own initialization. This event is raised when the base class has fully parsed and validated the passed in xml data. This is the correct moment to use if there is a need to do some additional initialization of your own handler.
CreateMessage() - In this abstract (MustInherit in Visual Basic) method the message to send is created. Deriving classes creates a message to send in this method. Typically this message is a Content Studio document but developers can create any message to by overriding this method.
ReadData - This method reads a list of subscribers that should receive a message. The data is read in pages and gets called until no more data is returned.
-
ValidateSubscriber(SubscriptionInformation) -
This method gets called for every subscriber returned by
ReadData(Int32). Developers override this method whenever there is a need to apply custom validating for each found subscriber. ValidateMessage(SubscriptionInformation) -
This method is called for every individual copy of the message to be sent out.
In this method implementations can process the message individually
for each subscriber returned by
ReadData(Int32) . SetMailMessageContent(String, String) - This metod is available for classes that inherit the
MailSubscriptionHandler class and lets the developer set a new mail message content to the message already created. Typically this method is overridden when there is a need to do your own replacement or formatting operations to the mail message content sent out. This metod is called once for each individual copy of the message to be sent out SendMessage(SubscriptionInformation) -
When the subscriber and her message is validated, the message will be sent out by this method.
Derived class implements their own send operation be it a regular mail message
or some other message system, such as SMS or fax.
For classes that inherit the
MailSubscriptionHandler or its derived classes, there is little use of overriding this method since they already provides a full implementation of the mail sending process. Finished - This event is fired at the end when the send operation has finshed successfully. Typically you will use this event to set the status message returned to the Content Studio Service Manager.
In addition to these action methods every implementation can call one of the meta data methods to provide the handler with information.
-
WriteToLog(SyslogPriority, String) - A developer can call this method to write her own messages to the log. If no logger has been specified, this method does nothing.
-
Internally this method checks if any logger exists and is writeable before it calls into the
WriteMessage(SyslogPriority, String) method of the providedISysLogWriter interface implementation. SetStatus -
Sets the status text that is sent back to the Service Manager after that the event handler has finished.
This text is passed to the Content Studio Service Manager and logged as a part of the success message,
for example, this might be the number of messages actually sent out and the number of
receivers that failed.
You should call this method in the
Finished event that is raised just after that the message has been sent to all subscribers. - Do not use this method to pass error messages, for more information on how to handle exceptions, see the Exception handling section in this article.
A sample handler
This section describes a custom subscription handler that sends mails to all enabled users that belong to a specific group in a directory server such as Active Directory.
Since handler uses the Content Studio document that triggered the event as a
source of the mail the main work will take place in the overidden
The implementation of this method requires that we supply three dynamic properties that contains the
data needed to find the specific group in the Directory. Additionally this
sample requires .NET Framework 3.5 and a reference to the assembly
System.DirectoryServices.AccountManagement in order to build.
-
myCorp.cs.eventHandlers.directoryMailHandler.ldapPath
specifies the ldap path (not including the schema part i.e. LDAP://).
Ex. OU=Personel,OU=Management,DC=mycorp,DC=com -
myCorp.cs.eventHandlers.directoryMailHandler.server
specifies the server that manages the directory.
For Active Directory this can be the DNS name of the directory ex. mycorp.com or the name of a Domain Controller. -
myCorp.cs.eventHandlers.directoryMailHandler.groupName
specifies the name of the group ex. Managers
For more information on dynamic properties in the Subscription Services see the Subscription services section in the Technical overview.
In addition to this the handler overrides a number of other methods mainly for the reason to add logging functionality.
using System;
using System.Collections.Generic;
using System.DirectoryServices.AccountManagement;
using ContentStudio.ServiceManager.Logging;
namespace MyCorp.CS.EventHandlers
{
public class DirectoryMailHandler : MailSubscriptionHandler
{
public DirectoryMailHandler()
: base(SubscriptionToHandle.Newsletter)
{
//Add a subscription on the Finished event
this.Finished += new FinishedEventHandler(OnFinished);
}
private void OnFinished(object sender, EventArgs e)
{
WriteToLog(SyslogPriority.Informational, "The mail sending operation has finished.");
string message = String.Format("OK: {0} mail(s) sent. {1} mail(s) could not be sent.", SendCount, FailedCount);
WriteToLog(SyslogPriority.Informational, message);
SetStatus(message);
WriteToLog(SyslogPriority.Informational, "Job finished successfully");
}
protected override IList<SubscriptionInformation> ReadData(int dataIndex)
{
List<SubscriptionInformation> theList = new List<SubscriptionInformation>();
//we do not use paging here, so just return an empty list
//if data index is anything but zero.
if(dataIndex != 0)
return theList;
//the OnTestDocumentSubscription event only sends mail to the supplied receiver
EventActions.ContentStudioEvents ev = (EventActions.ContentStudioEvents)EventId;
if (ev == ContentStudio.EventActions.ContentStudioEvents.OnTestDocumentSubscription)
{
SubscriptionInformation info =
SubscriptionInformation.Create(Guid.Empty,
Properties.Receiver,
base.SubscriptionTypeHandled,
SubscriptionInformation.TypeOfAddress.Mail,
String.Empty,
true);
theList.Add(info);
return theList;
}
//code to read from a specific OU in a directory
//this assumes that you a set of custom dynamic properties
string ldap = ExtendedProperties["myCorp.cs.eventHandlers.directoryMailHandler.ldapPath"];
string server = ExtendedProperties["myCorp.cs.eventHandlers.directoryMailHandler.server"];
string groupName = ExtendedProperties["myCorp.cs.eventHandlers.directoryMailHandler.groupName"];
//check the passed in data
if (String.IsNullOrEmpty(ldap))
throw new ArgumentException("Extended property \"myCorp.cs.eventHandlers.directoryMailHandler.ldapPath\" must be defined.");
if (String.IsNullOrEmpty(server))
throw new ArgumentException("Extended property \"myCorp.cs.eventHandlers.directoryMailHandler.server\" must be defined.");
if (String.IsNullOrEmpty(groupName))
throw new ArgumentException("Extended property \"myCorp.cs.eventHandlers.directoryMailHandler.groupName\" must be defined.");
using (PrincipalContext priCx = new PrincipalContext(ContextType.Domain, server, ldap))
{
PrincipalSearcher priS = new PrincipalSearcher();
using (GroupPrincipal theGroup = new GroupPrincipal(priCx))
{
//if the group does not exist, an ArgumentException will be thrown
theGroup.Name = groupName;
priS.QueryFilter = theGroup;
using (GroupPrincipal requestedGroup = priS.FindOne() as GroupPrincipal)
{
if (requestedGroup == null)
throw new ArgumentException(String.Format("Group {0} not found.", theGroup.Name));
//go through all groups to find all members
foreach (Principal pu in requestedGroup.Members)
{
using (UserPrincipal user = pu as UserPrincipal)
{
if (user != null && user.HasValue && user.Enabled.Value)
{
SubscriptionInformation sui =
SubscriptionInformation.Create(Guid.Empty,
user.EmailAddress,
base.SubscriptionTypeHandled,
SubscriptionInformation.TypeOfAddress.Mail,
user.Name,
true);
theList.Add(sui);
}
}
}
}
}
}
return theList;
}
protected override void CreateMessage()
{
base.CreateMessage();
WriteToLog(SyslogPriority.Informational, "A message to send has been created.");
WriteToLog(SyslogPriority.Informational, "Starting the mail sending operation.");
}
protected override bool SendMessage(SubscriptionInformation subscriber)
{
if (base.SendMessage(subscriber))
{
WriteToLog(SyslogPriority.Informational, String.Format("Mail sent to {0}.", subscriber));
return true;
}
WriteToLog(SyslogPriority.Warning, String.Format("Mail could not be sent to {0}.", subscriber));
return false;
}
}
}
The IDisposable interface
Since a subscription event handler probably will use resources such as file handles,
database connections or mail messages your event handler should implement the IDisposable
interface. That implementation must ensure that all resources are closed in order
to free system memory. When implemented, Content Studio Service Manager that calls
your implementation of the
The code sample is taken from the official MSDN documentation provided by Microsoft.
protected override void Dispose(bool disposing)
{
if (disposing)
{
// Release managed resources.
}
// Release unmanaged resources.
// Set large fields to null.
// Call Dispose on your base class.
base.Dispose(disposing);
}
Protected Overloads Overrides Sub Dispose(disposing As Boolean)
If disposed = False Then
If disposing Then
'Release managed resources.
End If
End If
'Release unmanaged resources.
'Set large fields to null.
'Call Dispose on your base class.
Mybase.Dispose(disposing)
End Sub
Creating an event handler from scratch
Implementing an event handler from scratch needs far more work and is much more difficult than extending one of the existing base classes. For this reason there is very little reason to do so unless you need complete control over the event handler. This process is documented here mainly for the reason to provide you with information on how a subscription event handler works internally.
When you implement an event handler from scratch you must provide you own implementation
of the
property name | value |
---|---|
receiver | The receiver of the message, only available for OnTestDocumentSubscription events |
subscriptiondefinitionid | A guid that identifies the subscription definition. You can use this value if you need to open up the definition for the document's category. |
baseurl | The url to use if the event handler need to browse to the document |
sender | The sender of the message |
presentationtemplate | If specified, the numeric indentifier of a presentaion template to use. Only used with Ept documents. |
server | The name or the IP-address of the message server. |
authenticationSchema | A value that indicates the authentication scheme to use. This value is one of the members if the AuthenticationSchemes enumeration. |
The customData parameter will contain all of the extended properties in one Xml document with the following format.
<properties>
<property name="myProperty" value="property value" />
<!-- more properties can follow -->
</properties>
For more information on the extended properties, see the Subscription Services section in the Technical Overview documentation.
When the arguments have been parsed you should create a message to send to your subscribers. A message can preferably be created from the content of the document passed in and be formatted according to your needs or combined with some external data of your choice. The format of your message is of course dependent on the type of message to send (ex. text mails, html mails or SMS messages) and the capability of the client that the user will use to read the message. For example mobile devices has very limited capabilities when it comes to reading Html mails - only a basic set of Html elements usually are supported and often CSS-formatting will not work.
The next step to implement is to get a list of subscribers to send the message to.
You can read this list from the Content Studio repository by using the
You can obtain the list of subscribers from any repository available such as the
Active Directory, an Microsoft Excel document or a plain text file.
You now should send the message, through the message server, to each one of the subscribers found. Before sending the message you might like to modify the message to make it more personal etc.
When the send operation is completed you can return a success message back to the Service Manager via the statusText output parameter. This status message should not be an error message - just some additional information back to the Service Manager such as the number of messages sent or so. All fatal exceptions should be thrown so that they can be catched by Service Manager and logged as errors in the Content Studio Event log.
Exception handling
The statusText output parameter of the
Whenever a critical condition occurs you should throw the exception or re-throw the exception so it can be catched by the Service Manager. Any unhandled exception in your event handler will be caught by the Service Manager and logged as failures in the Content Studio log. If you do catch and handle critical errors in you event handler and just exit the sending operation at that point and use the statusText parameter to pass the error information back to the Service Manager the job will be logged as a success, something that we do not want.
Installing and activating an Event handler
When you have created and fully debugged an assembly that implements your event handler you must provide information on how to create it and where to find the assembly. Since your implementation is called directly by the Content Studio Service Manager you just need to place your assembly in the same directory where the Service Manager (CSServMgr.exe) is installed. By default this is Program Files\Teknikhuset\Content Studio 5\CSServer. If your event handler inherit from one of the base classes provided you should specify the AssemblyQualifiedName of your assembly in the Custom Event handler field in the subscription properties tab of the category properties dialog in the Content Studio administrative interface. For more information on how to configure and set up a category for subscription, see the Subscription Services section in the Technical Overview documentation. If the AssemblyQualifiedName is correct and the Service Manager can find your assembly, the event handler will call your event handler when the OnDocumentSubscription event is handled. The format of AssemblyQualifiedNames has been thoroughly documented by Microsoft at this location AssemblyQualifiedName.
Creating a custom log writer
Often there is a need to supply background processes with some sort of logging functionality
and the easiest way of doing this is to create an assembly that implements the
The ISysLogWriter interface
The
The
bool CanWrite
{
get;
}
Your implementation of the read-only property
SyslogFacility Facility
{
get;
}
void Open(SyslogFacility facility,
IDictionary<string, string> properties,
IFormatProvider format
)
When you inherit one of the provided base classes, the
The first argument, facility, is used when you need to distinguish different log sources
from each other and Content Studio uses the value
If the initialization fails for some reason but you do not want to interrupt the
message sending operation, make sure that the
void WriteMessage(SyslogPriority priority,
string message
)
When you inherit one of the provided base classes, the
IDiposable interface
Since a log writer most likely will use resources that need to be closed too free system memory (ex. database connection or file handles) every LogWriter should provide an implementation of the IDisposable interface where the resources are freed. If you are unsure on how to implement this interface please make sure to read the official documentation from Microsoft for the IDisposable interface. Provided that you inherit from one of the base classes provided, or your event handler implements IDisposable Content Studio Service Manager will ensure that your implementation gets called when the event handler has finished its job.
A sample log writer
The sample below shows a simple log writer that writes messages to a file in the file system. This is the implementation included in Content Studio.
using System.IO;
using System;
using System.Globalization;
using System.Collections.Generic;
using ContentStudio.ServiceManager.Logging;
namespace MyNamespace.LogWriter
{
public class FileLogger : ISysLogWriter
{
StreamWriter writer;
string fileName;
SyslogFacility facility = SyslogFacility.Local0;
int row;
IFormatProvider format;
/// <summary>
/// Initializes a new instance of the <see cref="FileLogger"/> class.
/// </summary>
public FileLogger()
{
}
/// <summary>
/// Opens the file.
/// </summary>
/// <param name="directory">The directory.</param>
/// <param name="fileName">Name of the file.</param>
private void OpenFile(string directory, string fileName)
{
if (String.IsNullOrEmpty(directory))
directory = Path.GetTempPath(); // @"C:\Temp";
//the directory must exist
if(!Directory.Exists(directory))
throw new ArgumentException(String.Format("The directory \"{0}\" does not exist.", directory), "directory");
if(String.IsNullOrEmpty(fileName))
fileName = GenerateFileName();
//
if(!IsValidFileName(fileName))
throw new ArgumentException(String.Format("The file name \"{0}\" is not valid.", fileName), "fileName");
this.fileName = Path.Combine(directory, fileName);
}
private bool IsValidFileName(string name)
{
foreach(char c in Path.GetInvalidPathChars())
{
if(name.IndexOf(c) > -1)
return false;
}
return true;
}
private string GenerateFileName()
{
DateTime now = DateTime.Now;
return (String.Format("CSFL-{0}${1}.{2}", now.ToString("yyMMddHHmmss"), now.ToString("FFF") , "log"));
}
#region ISysLogWriter Members
/// <summary>
/// Gets a value indicating whether this instance can write to its log.
/// </summary>
/// <value><c>true</c> if this instance can write; otherwise, <c>false</c>.</value>
/// <remarks>
/// Implementation can use this value to indicate to the logging application whether or not
/// the log system is working. Caller should call this property to ensure that the
/// logging implementation is working properly.
/// </remarks>
public bool CanWrite
{
get
{
if(writer == null)
return false;
return true;
}
}
/// <summary>
/// Gets the syslog facility (source) in use.
/// </summary>
/// <value>The facility.</value>
public SyslogFacility Facility
{
get { return facility; }
}
/// <summary>
/// Opens the log with a specific facility, a formatter and prepares the log for writing.
/// </summary>
/// <param name="facility">The facility to use.</param>
/// <param name="properties">The properties passed in.</param>
/// <param name="format">The format.</param>
public void Open(SyslogFacility facility, IDictionary<string, string> properties, IFormatProvider format)
{
if (!Enum.IsDefined(typeof(SyslogFacility), facility))
throw new ArgumentOutOfRangeException("facility");
if (disposed)
throw new ObjectDisposedException("ISysLogWriter");
if (writer == null)
{
//Set the directory and file name to write to.
if (properties == null)
properties = new Dictionary<string, string>();
string directory;
string filename;
properties.TryGetValue("ContentStudio.ServiceManager.Logging.FileLogger.Directory", out directory);
properties.TryGetValue("ContentStudio.ServiceManager.Logging.FileLogger.FileName", out filename);
OpenFile(directory, filename);
try
{
writer = new StreamWriter(this.fileName, true, System.Text.Encoding.UTF8);
//Write a blank line to separated jobs from each other
writer.WriteLine();
}
catch (IOException)
{
}
if (format != null)
format = CultureInfo.CurrentCulture;
this.format = format;
}
else
throw new InvalidOperationException("Logger already open");
}
/// <summary>
/// Writes a message to the syslog.
/// </summary>
/// <param name="priority">The priority.</param>
/// <param name="message">The message.</param>
public void WriteMessage(SyslogPriority priority, string message)
{
if(!CanWrite)
throw new InvalidProgramException("Logger not open");
row++;
string m = String.Format("{0}\t{1}\t{2}\t{3}",
row.ToString(format),
DateTime.Now.ToString(format),
priority,
message.ToString(format));
writer.WriteLine(m);
writer.Flush();
}
#endregion
#region IDisposable Members
/// <summary>
/// Releases unmanaged resources and performs other cleanup operations before the
/// <see cref="FileLogger"/> is reclaimed by garbage collection.
/// </summary>
~FileLogger()
{
Dispose(false);
}
private bool disposed;
/// <summary>
/// Performs application-defined tasks associated with freeing
/// , releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
// This object will be cleaned up by the Dispose method.
// Therefore, you should call GC.SupressFinalize to
// take this object off the finalization queue
// and prevent finalization code for this object
// from executing a second time.
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and
/// unmanaged resources; <c>false</c> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
// Check to see if Dispose has already been called.
if (!this.disposed)
{
// If disposing equals true, dispose all managed resources.
if (disposing)
{
// Dispose managed resources.
if(writer != null)
writer.Dispose();
}
// Note disposing has been done.
disposed = true;
writer = null;
}
}
#endregion
}
}
Installing and enabling your log writer
When you have created and fully debugged an assembly that implements the ISysLogWriter interface you must provide information on how to create it and where to find the assembly. Since your implementation is called directly by the Content Studio Service Manager you just need to place your assembly in the same directory where the Service Manager (CSServMgr.exe) is installed. By default this is Program Files\Teknikhuset\Content Studio 5\CSServer. If your event handler inherit from one of the base classes provided you should specify the AssemblyQualifiedName of your assembly as the value of the ContentStudio.ServiceManager.Logging.ISysLogWriter extended property. Extended properties can be specified in the subscription properties tab of the category properties dialog in the Content Studio administrative interface. For more information on how to configure and set up a category for subscription see the Subscription Services section in the Technical Overview documentation. If the AssemblyQualifiedName is correct and the Service Manager can find your implementation the event handler will write to your log write whenever needed. The format of AssemblyQualifiedNames has been thoroughly documented by Microsoft at this location AssemblyQualifiedName.