When it comes to automating things in Microsoft Dynamics CRM, you usually create server-side logic in workflows or plug-ins. Workflows are great to perform simple tasks and can be greatly enhanced with custom workflow activities. However, they always need an entity context. Plug-Ins are great as well, but they are more complex. They also require an entity context. What if you want to automate a data import or export, or you want to schedule other tasks that are not triggered by an action in CRM?
Windows Services
A good alternative to workflows and plug-ins is a Windows Service and as I haven't seen an example of it so far, I'm doing it now, because it's really easy. Since I have my new notebook, I only have Visual Studio 2008 installed. If you are running an older version, you should still be able to follow this article, because the Windows Service template was available at least since Visual Studio 2003.
Create a new project and select the Windows Service template:
Note that I'm using the .NET Framework 3.5 (dropdown in the top right corner). I do have the .NET Framework installed on my server machines, so I'm not limiting me to the 3.0 Framework as recommended for other CRM server development. You can choose whatever you are comfortable with though. I'm not even sure if I'm using .NET 3.5 features, but as the application is not running in the context of CRM, I have the freedom of choice.
Everything else is a standard project setup, so choose a name, location and solution name and hit the OK button to create the new project.
You don't get very much when creating a new service project. The Program.cs file contains the startup code like in a console or Windows forms application. However, the code is slightly different:
The ServiceBase class is used to declare the services to run and ServiceBase.Run actually starts them. Each service implementation must inherit from ServiceBase like the Service1 class that was auto-generated as well:
There are two empty methods generated for us: OnStart and OnStop. Obviously these are called when the service is started or stopped. However, there are more methods you can implement, like OnPause, OnContinue or OnShutdown. I'm adding a few of them later in this article, but for now let's start with a basic implementation. The goal of this service is to write an export file whenever opportunities were created or updated. The CRM logic will be pretty simple here, because the sample itself concentrates on the service.
Before writing any code, let's change some service properties. Open the Service1.cs in design mode and change the (Name) and ServiceName properties:
The (Name) property defines the class name of our service and when changing it to CrmOpportunityService, then the Service1 class is renamed to CrmOpportunityService as well. The ServiceName property is the display name you see when looking at the Service Manager window. The various Canxxx properties define the characteristics of this service. If you want to support Pause and Continue, then set CanPauseAndContinue to true and implement the appropriate methods in the service class.
Finally, I renamed Service.cs to CrmOpportunityService.cs, because I like file names to be the same as the contained class. Now you can compile the project and you get a CrmOpportunityService.exe, which is your service. Great, isn't it?
Of course it's not. Even at this very early stage you might ask how to test the application and you are right. The most natural thing is hitting F5 to run the application:
Well, that seems correct, but it doesn't really help when writing code. You don't want installing the application as a service to see if it works, then stop and uninstall to go back to development. So here's a very easy workaround:
I'm using a new conditional compilation symbol named RUN_IN_VS and defined it in the debug configuration:
You could also check if the DEBUG symbol is defined, but I decided to go for a new symbol. The release build does not define the RUN_IN_VS symbol, so when compiling the release version, you can safely install the service and it will act as a service.
The ServiceBase class does not have a StartService method and I changed the service implementation as well:
OnStart is called when Windows starts our service and it calls the same StartService method we're calling directly from the main program when running in Visual Studio. You can now set a breakpoint on the StartService method and successfully run the code.
There's one thing missing though: as soon as the StartService method returns, the service is done. A real service, however, should run until it's stopped. If you compile the code in release mode, install the service and start it, then it will instantly go into the stopped state. To prevent it, we need a main execution thread.
The difference to the previous implementation is the MainExecutionThread. It's initialized when StartService is called and its target is the Run method. The Run method executes whatever the service is intended to do, until the StopCommandReceived property is set to true. In the current implementation this can only happen if OnStop is called. This is done by Windows when you manually stop a service or Windows shuts down. But each service also has a Stop method, defined in the base ServiceBase class. This allows a quick test of our implementation, by slightly changing the main program code:
After the service was started with the call to the StartService method, the thread waits for five seconds until it calls the service's Stop method, which then terminates the MainExecutionThread. When running the code you will notice that it stops after 5-7 seconds. This is because the MainExecutionThread itself sleeps for two seconds after it has done all of its work, which isn't anything at this time. So every two seconds it looks at the StopCommandReceived property. After 5 seconds the main application calls the Stop method of our service and the implementation in ServiceBase calls the OnStop method in our service, which finally sets StopCommandReceived to true. When that happens, our MainExecutionThread (the Run method) most probably is in its sleep state, so besides the 5 seconds from the main application, another 0-2 seconds are added from the Thread.Sleep(2000) in the Run method, giving a total of 5-7 seconds.
This was just for showing the basic idea. If our Run method was doing something valuable, then five seconds wouldn't be enough. Here's an easy fix:
Passing Timeout.Infinite to Thread.Sleep will cause the thread to wait forever, giving us enough time to test our service implementation. You can stop the execution by selecting the Stop Debugging command in Visual Studio. But as I want to have a real test, even while developing, including pause and continue, it's not good enough and we have to find another solution. Before doing that, it's time to implement something else though.
A Windows Service, a Plug-In and a Workflow are all server-based code. And server-based code doesn't have any UI. To know what your application is doing, you have to implement a good logging mechanism. This is often ignored while developing, because the debugger tells you most of what you need to know. But at some time you deploy the application in a production environment and sooner or later there will be a problem. Without any logging mechanism it's very difficult to troubleshoot, so add it at a very early stage.
A Windows service by default logs to the Windows Event Log. While developing though, it's too time-consuming looking at the event viewer. At least in my opinion. So we need at least two different logging implementations, one for the release build and one for debugging.
My first implementation is really easy and consists of a logging interface and a simple log implementation writing to the Visual Studio output window. Here's the interface:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
namespace CrmOpportunityService {
public interface ILog {
void Add(EventLogEntryType type, string message);
void Add(EventLogEntryType type, string message, params object[] args);
void Add(EventLogEntryType type, Exception x);
}
}
I'm using the EventLogEntryType enumeration from the System.Diagnostics namespace to signal the event type, because it's used when writing to the Windows event log. The Visual Studio logger is as simple as the interface:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Web.Services.Protocols;
using System.Net;
namespace CrmOpportunityService {
public class VsDebugLog : ILog {
public VsDebugLog() {
}
#region ILog Members
public void Add(System.Diagnostics.EventLogEntryType type, string message) {
Debug.WriteLine(message, type.ToString());
}
public void Add(System.Diagnostics.EventLogEntryType type, string message, params object[] args) {
Debug.WriteLine(string.Format(message, args), type.ToString());
}
public void Add(System.Diagnostics.EventLogEntryType type, Exception x) {
if (x is SoapException) {
SoapException e = x as SoapException;
string details = (e.Detail != null) ? e.Detail.InnerXml : "none";
Debug.WriteLine(e.Message + "\r\nDetails: " + details, type.ToString());
}
Debug.WriteLine(x.ToString());
}
#endregion
}
}
The only special thing when logging an exception is the check for a SoapException. If something goes wrong when calling the CRM web service, a SoapException is thrown and the exception message usually contains "Server was unable to process request", while the details are available in the Details node and it's invaluable to include these details in the log file.
I'm adding appropriate code for writing to the Windows event log later. While still in development mode and far away from a release version, I added the following to the service implementation:
ILog Log { get; set; }
public CrmOpportunityService() {
InitializeComponent();
this.Log = new VsDebugLog();
}
private void Run() {
this.StopCommandReceived = false;
this.Log.Add(EventLogEntryType.Information, "Service started.");
while (!this.StopCommandReceived) {
try {this.Log.Add(EventLogEntryType.Information, "Executing job");// do workthis.Log.Add(EventLogEntryType.Information, "Finished job");
}catch (Exception x) {this.Log.Add(EventLogEntryType.Error, x);
}
Thread.Sleep(2000);
}
this.MainExecutionThread = null;
this.Log.Add(EventLogEntryType.Information, "Service stopped.");
}
It's up to you what to include in the log file, but I wanted to have some output and added more log entries than needed. The entire "processing" code is wrapped into a try-catch block. If the code we are going to implement throws an exception that we haven't handled elsewhere, it is written to the log and the service doesn't break.
Running the code in the Visual Studio environment now produces the following output in the output window:
'CrmOpportunityService.vshost.exe' (Managed): Loaded 'D:\BLOG\CRM 4.0 Samples\CrmOpportunityService\CrmOpportunityService\bin\Debug\CrmOpportunityService.exe', Symbols loaded.
'CrmOpportunityService.vshost.exe' (Managed): Loaded 'C:\Windows\assembly\GAC_MSIL\System.Configuration\2.0.0.0__b03f5f7f11d50a3a\System.Configuration.dll', Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Information: Service started.
Information: Executing job
Information: Finished job
Information: Executing job
Information: Finished job
Information: Executing job
Information: Finished job
Information: Stop command received.
The thread 0x1f8 has exited with code 0 (0x0).
The thread 0x1b7c has exited with code 0 (0x0).
Information: Service stopped.
The thread 0x1584 has exited with code 0 (0x0).
The thread 0x1b44 has exited with code 0 (0x0).
The program '[5068] CrmOpportunityService.vshost.exe: Managed' has exited with code 0 (0x0).
Again, it's a very simple log implementation, but you can see what commands are sent to the service, how it's working and behaving. Still not good enough, because I want to control the service the way it's used when deployed. So let's add our own service control manager application.
The Service Control Manager
It's a simple Windows form with four buttons on it for starting, stopping, pausing and resuming the service. I'm not explaining how to create a Windows Forms application. Instead I'm just showing the relevant code, which, in the beginning, only has implementations for the Start and Stop buttons:
public partial class ServiceControlManagerForm : Form, ILog {
CrmOpportunityService.CrmOpportunityService Service { get; set; }
StringBuilder NewLogEntries { get; set; }
object NewLogEntriesLock { get; set; }
public ServiceControlManagerForm() {
InitializeComponent();
this.btnStartService.Enabled = true;
this.btnStopService.Enabled = false;
this.btnPauseService.Enabled = false;
this.btnContinueService.Enabled = false;
this.NewLogEntries = new StringBuilder();
this.NewLogEntriesLock = new object();
}
private void btnStartService_Click(object sender, EventArgs e) {
if (this.Service == null) {
this.btnStartService.Enabled = false;
this.btnStopService.Enabled = true;
this.btnPauseService.Enabled = true;
this.btnContinueService.Enabled = false;
this.Service = new CrmOpportunityService.CrmOpportunityService(this);
this.Service.StartService();
}
}
private void btnStopService_Click(object sender, EventArgs e) {
if (this.Service != null) {
this.btnStartService.Enabled = true;
this.btnStopService.Enabled = false;
this.btnPauseService.Enabled = false;
this.btnContinueService.Enabled = false;
this.Service.Stop();
this.Service = null;
}
}
private void logTimer_Tick(object sender, EventArgs e) {
if (this.NewLogEntries.Length > 0) {
lock (this.NewLogEntriesLock) {
this.txtLog.Text = this.txtLog.Text + this.NewLogEntries.ToString();
this.txtLog.SelectionStart = this.txtLog.Text.Length - 1;
this.txtLog.SelectionLength = 0;
this.txtLog.ScrollToCaret();
this.NewLogEntries = new StringBuilder();
}
}
}
private void AddLogEntry(string message) {
lock (this.NewLogEntriesLock) {
this.NewLogEntries.AppendLine(message);
}
}
#region ILog Members
public void Add(System.Diagnostics.EventLogEntryType type, string message) {
this.AddLogEntry(type.ToString() + ": " + message);
}
public void Add(System.Diagnostics.EventLogEntryType type, string message, params object[] args) {
this.AddLogEntry(type.ToString() + ": " + string.Format(message, args));
}
public void Add(System.Diagnostics.EventLogEntryType type, Exception x) {
if (x is SoapException) {
SoapException e = x as SoapException;
string details = (e.Detail != null) ? e.Detail.InnerXml : "none";
this.AddLogEntry(type.ToString() + ": " + e.Message + "\r\nDetails: " + details);
}
this.AddLogEntry(x.ToString());
}
#endregion
}
The event handlers for the start and stop buttons should be self-explanatory. The important thing is the implementation of the ILog interface, allowing the form to serve as a log for our service implementation. To make it available in the service class, I added a second constructor and modified the first implementation:
public CrmOpportunityService()
: this(new VsDebugLog()) {
}
public CrmOpportunityService(ILog log) {
InitializeComponent();
this.Log = log;
}
Besides the four buttons on the form, there's also a timer control raising the Tick event every 250ms. You cannot access a Windows form from another thread and as the log messages are generated from the MainExecutionThread in our service class, it's not possible to simply append the messages to the text field, which is used to display them. Instead, I'm using a StringBuilder object and add the new messages to it. The logTimer_Tick method checks for new messages, adds them to the text box and removes them from the buffer. The lock statements are used to synchronize this process.
Running the form now allows starting and stopping the service and it also shows the log messages:
Supporting Pause and Continue
Now let's add pause and continue to the code. All we have to do is overriding the OnPause and OnContinue methods in the service class and add appropriate handling to our main execution thread:
namespace CrmOpportunityService {
public partial class CrmOpportunityService : ServiceBase {
...
bool Paused { get; set; }
...
protected override void OnPause() {this.Log.Add(EventLogEntryType.Information, "Pause command received.");this.Paused = true;
}protected override void OnContinue() {this.Log.Add(EventLogEntryType.Information, "Continue command received.");this.Paused = false;
}public void PauseService() {this.OnPause();
}public void ContinueService() {this.OnContinue();
}
private void Run() {
this.StopCommandReceived = false;
this.Paused = false;
this.Log.Add(EventLogEntryType.Information, "Service started.");
while (!this.StopCommandReceived) {
if (!this.Paused) {
try {
this.Log.Add(EventLogEntryType.Information, "Executing job");
// do work
this.Log.Add(EventLogEntryType.Information, "Finished job");
}
catch (Exception x) {
this.Log.Add(EventLogEntryType.Error, x);
}
}
Thread.Sleep(2000);
}
this.MainExecutionThread = null;
this.Log.Add(EventLogEntryType.Information, "Service stopped.");
}
}
}
OnPause and OnContinue simply set a variable to either true or false and the Run method stops its execution when entering the pause state. PauseService and ContinueService are helper methods, allowing us to execute the Pause and Continue commands from our own Service Control Manager application. The last step is adding appropriate code to the Pause and Continue buttons in the Service Control Manager:
private void btnPauseService_Click(object sender, EventArgs e) {
if (this.Service != null) {
this.btnStartService.Enabled = false;
this.btnStopService.Enabled = true;
this.btnPauseService.Enabled = false;
this.btnContinueService.Enabled = true;
this.Service.PauseService();
}
}
private void btnContinueService_Click(object sender, EventArgs e) {
if (this.Service != null) {
this.btnStartService.Enabled = false;
this.btnStopService.Enabled = true;
this.btnPauseService.Enabled = true;
this.btnContinueService.Enabled = false;
this.Service.ContinueService();
}
}
To let Windows know that our service supports Pause and Continue, open the service class in the design view and set the CanPauseAndContinue property to true:
Implementing the CRM code
We can now start, pause, continue and stop our service and do the CRM implementation. The service is intended to run on a CRM server machine (though you can run it on any machine) and as any CRM 4.0 server has the CRM SDK assemblies installed in the GAC, I'm using them in the service project instead of adding a web reference. However, you can also add a reference to the CRM web services and use it instead of the SDK assemblies.
I'm entirely separating the CRM code from the service code to make the service framework more reusable:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
namespace CrmOpportunityService {
public class CrmOpportunityExport {
ILog Log { get; set; }
public CrmOpportunityExport(ILog log) {
this.Log = log;
}
public void Execute() {
}
}
}
The only modification to the service code is in the Run method:
private void Run() {
this.StopCommandReceived = false;
this.Paused = false;
this.Log.Add(EventLogEntryType.Information, "Service started.");
while (!this.StopCommandReceived) {
if (!this.Paused) {
try {
this.Log.Add(EventLogEntryType.Information, "Executing job");
CrmOpportunityExport export = new CrmOpportunityExport(this.Log);
export.Execute();
this.Log.Add(EventLogEntryType.Information, "Finished job");
}
catch (Exception x) {
this.Log.Add(EventLogEntryType.Error, x);
}
}
Thread.Sleep(2000);
}
this.MainExecutionThread = null;
this.Log.Add(EventLogEntryType.Information, "Service stopped.");
}
The CrmOpportunityExport class of course needs access to the log file, which is the reason why I'm passing the log instance in the constructor. The Execute is quite simple, but I'm going to explain some helper methods in more detail:
public void Execute() {
DateTime now = DateTime.Now;
CrmService service = new CrmService();
service.Url = this.Configuration.ServerUrl + "/2007/crmservice.asmx";
service.UseDefaultCredentials = true;
service.CrmAuthenticationTokenValue = new CrmAuthenticationToken();
service.CrmAuthenticationTokenValue.AuthenticationType = 0;
service.CrmAuthenticationTokenValue.OrganizationName = this.Configuration.Organization;
QueryExpression query = new QueryExpression(EntityName.opportunity.ToString());
query.ColumnSet.AddColumns("estimatedclosedate", "estimatedvalue", "name", "opportunityid");
query.Criteria.AddCondition("modifiedon", ConditionOperator.LessEqual, now.ToString("s"));
if (this.Configuration.LastRun > DateTime.MinValue) {
query.Criteria.AddCondition("modifiedon", ConditionOperator.GreaterThan, this.Configuration.LastRun.ToString("s"));
}
BusinessEntityCollection opportunities = service.RetrieveMultiple(query);
if (opportunities.BusinessEntities.Count == 0) {
this.Log.Add(EventLogEntryType.Information, "No new or updated opportunities since " + this.Configuration.LastRun);
}
else {
this.Log.Add(EventLogEntryType.Information, opportunities.BusinessEntities.Count + " new or updated opportunities since " + this.Configuration.LastRun);
string fileContent = this.BuildExportFileContent(opportunities);
this.WriteExportFile(fileContent);
this.Configuration.LastRun = now;
}
}
The code retrieves all opportunities modified since the last export and this information has to be persisted. The easiest way to configure a service is the registry and I created a helper class reading the configuration values:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Win32;
using System.Configuration;
using System.Diagnostics;
namespace CrmOpportunityService {
public class ServiceConfiguration {
public const string ServiceRegistryRoot = @"Software\Stunnware\Samples\CrmOpportunityExport";
private DateTime _lastRun;
public string ServerUrl { get; private set; }
public string Organization { get; private set; }
public string ExportDirectory { get; private set; }
public int TimeoutBetweenRunsInSeconds { get; private set; }
public ServiceConfiguration(ILog log) {
RegistryKey serviceRoot = Registry.LocalMachine.OpenSubKey(ServiceRegistryRoot);
if (serviceRoot == null) {
throw new ConfigurationErrorsException("The HKLM\\" + ServiceRegistryRoot + " key does not exist in the registry.");
}
this.ServerUrl = this.ReadNonEmptyStringValue(serviceRoot, "ServerUrl");
this.Organization = this.ReadNonEmptyStringValue(serviceRoot, "Organization");
this.ExportDirectory = this.ReadNonEmptyStringValue(serviceRoot, "ExportDirectory");
object lastRun = serviceRoot.GetValue("LastRun");
if (lastRun != null) {
long ticks = long.Parse(lastRun.ToString());
this.LastRun = new DateTime(ticks);
}
else {
this.LastRun = DateTime.MinValue;
log.Add(EventLogEntryType.Warning, "The LastRun value does not exist in the HKLM\\" + ServiceConfiguration.ServiceRegistryRoot + " key of the registry. Exporting all opportunities.");
}
object timeoutBetweenRunsInSeconds = serviceRoot.GetValue("TimeoutBetweenRunsInSeconds");
if (timeoutBetweenRunsInSeconds != null) {
this.TimeoutBetweenRunsInSeconds = int.Parse(timeoutBetweenRunsInSeconds.ToString());
}
else {
this.TimeoutBetweenRunsInSeconds = 600;
log.Add(EventLogEntryType.Warning, "The TimeoutBetweenRunsInSeconds value does not exist in the HKLM\\" + ServiceConfiguration.ServiceRegistryRoot + " key of the registry. Setting to " + this.TimeoutBetweenRunsInSeconds + " seconds.");
}
}
private string ReadNonEmptyStringValue(RegistryKey serviceRoot, string name) {
string value = (string)serviceRoot.GetValue(name);
if (string.IsNullOrEmpty(value)) {
throw new ConfigurationErrorsException("The " + name + " value does not exist in the HKLM\\" + ServiceRegistryRoot + " key of the registry.");
}
return value;
}
public DateTime LastRun {
get {
return this._lastRun;
}
set {
this._lastRun = value;
RegistryKey serviceRoot = Registry.LocalMachine.CreateSubKey(ServiceRegistryRoot);
serviceRoot.SetValue("LastRun", value.Ticks);
}
}
}
}
Remember that the code will run on a server machine without any UI. Good error messages are a live saver when it comes to troubleshooting, so add as much information as possible. If a customer reads the Windows Event Log and finds a message that the CRM organization isn't configured and you also tell exactly where to add this information, then they may be able to solve the problem themselves. If you don't provide this information, then you most likely get a support call.
The ServiceConfiguration class introduces a new setting, the TimeoutBetweenRunsInSeconds. It is used in the main execution thread and controls how long the thread sleeps after having done it's job:
private void Run() {
this.StopCommandReceived = false;
this.Paused = false;
this.Log.Add(EventLogEntryType.Information, "Service started.");
ServiceConfiguration configuration = null;
try {
configuration = new ServiceConfiguration(this.Log);
}
catch (Exception x) {
this.Log.Add(EventLogEntryType.Error, x);
}
if (configuration != null) {
int remainingSeconds = 0;
while (!this.StopCommandReceived) {
Thread.Sleep(1000);
remainingSeconds--;
if (!this.Paused && (remainingSeconds <= 0)) {
try {
this.Log.Add(EventLogEntryType.Information, "Executing job");
CrmOpportunityExport export = new CrmOpportunityExport(this.Log);
export.Execute();
this.Log.Add(EventLogEntryType.Information, "Finished job");
}
catch (Exception x) {
this.Log.Add(EventLogEntryType.Error, x);
}
remainingSeconds = configuration.TimeoutBetweenRunsInSeconds;
}
}
}
this.MainExecutionThread = null;
this.Log.Add(EventLogEntryType.Information, "Service stopped.");
}
The previous implementation suspended the thread for 2000ms, which is acceptable when responding to a stop command. But say the service is configured to wait for 10 minutes after it has done its job, then stopping the service may also take up to 10 minutes, because the check for StopCommandReceived is executed after the sleep phase. To prevent such things, the thread is sleeping for one second and after that decrements the remainingSeconds variable. If it's down to 0, the job is executed and remainingSeconds is reset to the value configured in the registry.
The above implementation will instantly start the job, which, in most of the cases, is what you want. But it's just initializing remainingSeconds with configuration.TimeoutBetweenRunsInSeconds instead of setting it to 0 to wait in the beginning as well. You can easily add a new registry flag to even make it configurable.
All that's left is the export file generation, which is a very basic implementation:
private void WriteExportFile(string content) {
string filename = "ex" + DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss") + ".csv";
string path = Path.Combine(this.Configuration.ExportDirectory, filename);
FileInfo file = new FileInfo(path);
if (!file.Directory.Exists) {
file.Directory.Create();
}
using (FileStream fs = file.OpenWrite()) {
using (StreamWriter w = new StreamWriter(fs, Encoding.UTF8)) {
w.Write(content);
this.Log.Add(EventLogEntryType.Information, "Export file written to " + file.FullName);
}
}
}
private string BuildExportFileContent(BusinessEntityCollection opportunities) {
StringBuilder fileContent = new StringBuilder();
fileContent.AppendLine("opportunityid;name;estimatedclosedate;estimatedvalue");
foreach (opportunity o in opportunities.BusinessEntities) {
string estimatedCloseDate = (o.estimatedclosedate == null) ? null : o.estimatedclosedate.UniversalTime.ToString("u");
decimal estimatedValue = (o.estimatedvalue == null) ? 0 : o.estimatedvalue.Value;
fileContent.AppendLine(o.opportunityid.Value + ";" + o.name + ";" + estimatedCloseDate + ";" + estimatedValue);
}
return fileContent.ToString();
}
To test the code in development mode, we first run our Service Control Manager without configuring any values in the registry. This is just a test to see if our error handling is correct:
And the final test is adding the values to the registry:
After the values have been configured, the service should work in your development environment:
Supporting the Windows Event Log
Now it's time to plan for production. Our own Service Control Manager is great to test the service, but when running as a service, we cannot log to a Windows forms application. We have to implement Windows Event Logging, but you will see that it's damned easy:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Web.Services.Protocols;
using System.Net;
namespace CrmOpportunityService {
public class WindowsEventLog : ILog {
EventLog Log { get; set; }
public WindowsEventLog(EventLog log) {
this.Log = log;
}
#region ILog Members
public void Add(System.Diagnostics.EventLogEntryType type, string message) {
this.Log.WriteEntry(message, type);
}
public void Add(System.Diagnostics.EventLogEntryType type, string message, params object[] args) {
this.Log.WriteEntry(string.Format(message, args), type);
}
public void Add(System.Diagnostics.EventLogEntryType type, Exception x) {
if (x is SoapException) {
SoapException e = x as SoapException;
string details = (e.Detail != null) ? e.Detail.InnerXml : "none";
this.Log.WriteEntry(e.Message + "\r\nDetails: " + details, type);
}
}
#endregion
}
}
It's almost the same implementation as used in the other logs. The EventLog parameter is a member of each service class and we have to do a last change to our service implementation to run as a service:
public CrmOpportunityService() {
InitializeComponent();
this.Log = new WindowsEventLog(this.EventLog);
}
public CrmOpportunityService(ILog log) {
InitializeComponent();
this.Log = log;
}
When started as a service, the first constructor will be executed and we use the Windows Event Log to log our messages. When called from our own Service Control Manager application or using the VsDebugLog class, then the second constructor is used and we can look at the output without looking at the event log and hitting the refresh button every few seconds.
Installing the Service
Now it's time for the final test: running the application as a service. In order to do that, we have to install the service first. Open the designer view of the service class and right-click onto it:
The "Add Installer" option is what we need to configure our service and it's also available as a hyperlink in the properties window. Two components are added to our service: serviceInstaller1 and serviceProcessInstaller. The service installer contains most properties we need and I have set some typical values:
The service process installer defines the account the service is using. The default of "User" is what we need, so I haven't changed anything.
You can change this value to let the service run as the Local Service account, Local System account or Network Service account. However, we need a valid CRM user to successfully retrieve data from our CRM system and that means a user account.
Remember the conditional compilation symbol in the program.cs file? You have to un-define the RUN_IN_VS symbol to execute the ServiceBase.Run method instead of our own startup code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading;
namespace CrmOpportunityService {
static class Program {
/// <summary>
/// The main entry point for the application.
/// </summary>
static void Main() {
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[]
{
new CrmOpportunityService()
};
#if RUN_IN_VS
CrmOpportunityService service = new CrmOpportunityService();
service.StartService();
Thread.Sleep(5000);
service.Stop();
#else
ServiceBase.Run(ServicesToRun);
#endif
}
}
}
I only added the RUN_IN_VS symbol to the debug configuration, so all I had to do was switching to the release configuration.
Installing the service is pretty easy. The .NET Framework SDK has a utility named "installutil". It installs or uninstalls a service by either specifying the /i or the /u command line parameter. To register the release build as a service, start a command prompt and navigate to the directory containing the installutil.exe executable, which will be C:\Windows\Microsoft.NET\Framework64\v2.0.50727 or something similar (Framework64 if it's a 64-bit machine, otherwise just Framework). If you have Visual Studio installed, then simply run a Visual Studio Command Prompt and the PATH variable is set accordingly.
In the command prompt type installutil /i "full path to the service executable", e.g.
installutil /i "D:\BLOG\CRM 4.0 Samples\CrmOpportunityService\CrmOpportunityService\bin\Release\CrmOpportunityService.exe"
When inside a Visual Studio Command Prompt, you can also navigate to the directory containing the service executable and simply type
installutil /i CrmOpportunityService.exe
The specified parameters are shown in the console window and the installation starts:
>installutil /i CrmOpportunityService.exe
Microsoft (R) .NET Framework Installation utility Version 2.0.50727.3053
Copyright (c) Microsoft Corporation. All rights reserved.
Running a transacted installation.
Beginning the Install phase of the installation.
See the contents of the log file for the D:\BLOG\CRM 4.0 Samples\CrmOpportunityService\CrmOpportunityService\bin\Release\CrmOpportunityService.exe assembly's progress.
The file is located at D:\BLOG\CRM 4.0 Samples\CrmOpportunityService\CrmOpportunityService\bin\Release\CrmOpportunityService.InstallLog.
Installing assembly 'D:\BLOG\CRM 4.0 Samples\CrmOpportunityService\CrmOpportunityService\bin\Release\CrmOpportunityService.exe'.
Affected parameters are:
logtoconsole =
assemblypath = D:\BLOG\CRM 4.0 Samples\CrmOpportunityService\CrmOpportunityService\bin\Release\CrmOpportunityService.exe
i =
logfile = D:\BLOG\CRM 4.0 Samples\CrmOpportunityService\CrmOpportunityService\bin\Release\CrmOpportunityService.InstallLog
At this point a dialog pops up asking to provide the user credentials used to run the service:
And finally you get two more lines in the console window:
The Commit phase completed successfully.
The transacted install has completed.
That's it. You should now see the service in the Services window:
Start the service and open the event log to see the service event messages:
The Final Test
Our service is running now, but it doesn't mean it's working. To test, simply create a new opportunity or change an existing one and you should see a new export file in the export directory:
Because of the csv extension, a double-click opens the file in Microsoft Excel (if installed):
That's it. The entire source code is available for download and I hope that it helps you writing your own services in the future. Even if they are not related to CRM, because the majority of the code can be used for any service implementation
Windows Services
A good alternative to workflows and plug-ins is a Windows Service and as I haven't seen an example of it so far, I'm doing it now, because it's really easy. Since I have my new notebook, I only have Visual Studio 2008 installed. If you are running an older version, you should still be able to follow this article, because the Windows Service template was available at least since Visual Studio 2003.
Create a new project and select the Windows Service template:
Note that I'm using the .NET Framework 3.5 (dropdown in the top right corner). I do have the .NET Framework installed on my server machines, so I'm not limiting me to the 3.0 Framework as recommended for other CRM server development. You can choose whatever you are comfortable with though. I'm not even sure if I'm using .NET 3.5 features, but as the application is not running in the context of CRM, I have the freedom of choice.
Everything else is a standard project setup, so choose a name, location and solution name and hit the OK button to create the new project.
You don't get very much when creating a new service project. The Program.cs file contains the startup code like in a console or Windows forms application. However, the code is slightly different:
using System; using System.Collections.Generic; using System.Linq; using System.ServiceProcess; using System.Text; namespace CrmOpportunityService { static class Program { ////// The main entry point for the application. /// static void Main() { ServiceBase[] ServicesToRun; ServicesToRun = new ServiceBase[] { new Service1() }; ServiceBase.Run(ServicesToRun); } } }
The ServiceBase class is used to declare the services to run and ServiceBase.Run actually starts them. Each service implementation must inherit from ServiceBase like the Service1 class that was auto-generated as well:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Linq; using System.ServiceProcess; using System.Text; namespace CrmOpportunityService { public partial class Service1 : ServiceBase { public Service1() { InitializeComponent(); } protected override void OnStart(string[] args) { } protected override void OnStop() { } } }
There are two empty methods generated for us: OnStart and OnStop. Obviously these are called when the service is started or stopped. However, there are more methods you can implement, like OnPause, OnContinue or OnShutdown. I'm adding a few of them later in this article, but for now let's start with a basic implementation. The goal of this service is to write an export file whenever opportunities were created or updated. The CRM logic will be pretty simple here, because the sample itself concentrates on the service.
Before writing any code, let's change some service properties. Open the Service1.cs in design mode and change the (Name) and ServiceName properties:
The (Name) property defines the class name of our service and when changing it to CrmOpportunityService, then the Service1 class is renamed to CrmOpportunityService as well. The ServiceName property is the display name you see when looking at the Service Manager window. The various Canxxx properties define the characteristics of this service. If you want to support Pause and Continue, then set CanPauseAndContinue to true and implement the appropriate methods in the service class.
Finally, I renamed Service.cs to CrmOpportunityService.cs, because I like file names to be the same as the contained class. Now you can compile the project and you get a CrmOpportunityService.exe, which is your service. Great, isn't it?
Of course it's not. Even at this very early stage you might ask how to test the application and you are right. The most natural thing is hitting F5 to run the application:
Well, that seems correct, but it doesn't really help when writing code. You don't want installing the application as a service to see if it works, then stop and uninstall to go back to development. So here's a very easy workaround:
using System; using System.Collections.Generic; using System.Linq; using System.ServiceProcess; using System.Text; using System.Threading; namespace CrmOpportunityService { static class Program { ////// The main entry point for the application. /// static void Main() { ServiceBase[] ServicesToRun; ServicesToRun = new ServiceBase[] { new CrmOpportunityService() }; #if RUN_IN_VS CrmOpportunityService service = new CrmOpportunityService(); service.StartService(); #else ServiceBase.Run(ServicesToRun); #endif } } }
I'm using a new conditional compilation symbol named RUN_IN_VS and defined it in the debug configuration:
You could also check if the DEBUG symbol is defined, but I decided to go for a new symbol. The release build does not define the RUN_IN_VS symbol, so when compiling the release version, you can safely install the service and it will act as a service.
The ServiceBase class does not have a StartService method and I changed the service implementation as well:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Linq; using System.ServiceProcess; using System.Text; namespace CrmOpportunityService { public partial class CrmOpportunityService : ServiceBase { public CrmOpportunityService() { InitializeComponent(); } protected override void OnStart(string[] args) { this.StartService(); } protected override void OnStop() { } public void StartService() { } } }
OnStart is called when Windows starts our service and it calls the same StartService method we're calling directly from the main program when running in Visual Studio. You can now set a breakpoint on the StartService method and successfully run the code.
There's one thing missing though: as soon as the StartService method returns, the service is done. A real service, however, should run until it's stopped. If you compile the code in release mode, install the service and start it, then it will instantly go into the stopped state. To prevent it, we need a main execution thread.
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Linq; using System.ServiceProcess; using System.Text; namespace CrmOpportunityService { public partial class CrmOpportunityService : ServiceBase { public CrmOpportunityService() { InitializeComponent(); } protected override void OnStart(string[] args) { this.StartService(); } protected override void OnStop() { } public void StartService() { } } }
The difference to the previous implementation is the MainExecutionThread. It's initialized when StartService is called and its target is the Run method. The Run method executes whatever the service is intended to do, until the StopCommandReceived property is set to true. In the current implementation this can only happen if OnStop is called. This is done by Windows when you manually stop a service or Windows shuts down. But each service also has a Stop method, defined in the base ServiceBase class. This allows a quick test of our implementation, by slightly changing the main program code:
using System; using System.Collections.Generic; using System.Linq; using System.ServiceProcess; using System.Text; using System.Threading; namespace CrmOpportunityService { static class Program { ////// The main entry point for the application. /// static void Main() { ServiceBase[] ServicesToRun; ServicesToRun = new ServiceBase[] { new CrmOpportunityService() }; #if RUN_IN_VS CrmOpportunityService service = new CrmOpportunityService(); service.StartService(); Thread.Sleep(5000); service.Stop(); #else ServiceBase.Run(ServicesToRun); #endif } } }
After the service was started with the call to the StartService method, the thread waits for five seconds until it calls the service's Stop method, which then terminates the MainExecutionThread. When running the code you will notice that it stops after 5-7 seconds. This is because the MainExecutionThread itself sleeps for two seconds after it has done all of its work, which isn't anything at this time. So every two seconds it looks at the StopCommandReceived property. After 5 seconds the main application calls the Stop method of our service and the implementation in ServiceBase calls the OnStop method in our service, which finally sets StopCommandReceived to true. When that happens, our MainExecutionThread (the Run method) most probably is in its sleep state, so besides the 5 seconds from the main application, another 0-2 seconds are added from the Thread.Sleep(2000) in the Run method, giving a total of 5-7 seconds.
This was just for showing the basic idea. If our Run method was doing something valuable, then five seconds wouldn't be enough. Here's an easy fix:
#if RUN_IN_VS CrmOpportunityService service = new CrmOpportunityService(); service.StartService(); Thread.Sleep(Timeout.Infinite); service.Stop(); #else ServiceBase.Run(ServicesToRun); #endif
Passing Timeout.Infinite to Thread.Sleep will cause the thread to wait forever, giving us enough time to test our service implementation. You can stop the execution by selecting the Stop Debugging command in Visual Studio. But as I want to have a real test, even while developing, including pause and continue, it's not good enough and we have to find another solution. Before doing that, it's time to implement something else though.
Logging
A Windows Service, a Plug-In and a Workflow are all server-based code. And server-based code doesn't have any UI. To know what your application is doing, you have to implement a good logging mechanism. This is often ignored while developing, because the debugger tells you most of what you need to know. But at some time you deploy the application in a production environment and sooner or later there will be a problem. Without any logging mechanism it's very difficult to troubleshoot, so add it at a very early stage.
A Windows service by default logs to the Windows Event Log. While developing though, it's too time-consuming looking at the event viewer. At least in my opinion. So we need at least two different logging implementations, one for the release build and one for debugging.
My first implementation is really easy and consists of a logging interface and a simple log implementation writing to the Visual Studio output window. Here's the interface:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
namespace CrmOpportunityService {
public interface ILog {
void Add(EventLogEntryType type, string message);
void Add(EventLogEntryType type, string message, params object[] args);
void Add(EventLogEntryType type, Exception x);
}
}
I'm using the EventLogEntryType enumeration from the System.Diagnostics namespace to signal the event type, because it's used when writing to the Windows event log. The Visual Studio logger is as simple as the interface:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Web.Services.Protocols;
using System.Net;
namespace CrmOpportunityService {
public class VsDebugLog : ILog {
public VsDebugLog() {
}
#region ILog Members
public void Add(System.Diagnostics.EventLogEntryType type, string message) {
Debug.WriteLine(message, type.ToString());
}
public void Add(System.Diagnostics.EventLogEntryType type, string message, params object[] args) {
Debug.WriteLine(string.Format(message, args), type.ToString());
}
public void Add(System.Diagnostics.EventLogEntryType type, Exception x) {
if (x is SoapException) {
SoapException e = x as SoapException;
string details = (e.Detail != null) ? e.Detail.InnerXml : "none";
Debug.WriteLine(e.Message + "\r\nDetails: " + details, type.ToString());
}
Debug.WriteLine(x.ToString());
}
#endregion
}
}
The only special thing when logging an exception is the check for a SoapException. If something goes wrong when calling the CRM web service, a SoapException is thrown and the exception message usually contains "Server was unable to process request", while the details are available in the Details node and it's invaluable to include these details in the log file.
I'm adding appropriate code for writing to the Windows event log later. While still in development mode and far away from a release version, I added the following to the service implementation:
ILog Log { get; set; }
public CrmOpportunityService() {
InitializeComponent();
this.Log = new VsDebugLog();
}
private void Run() {
this.StopCommandReceived = false;
this.Log.Add(EventLogEntryType.Information, "Service started.");
while (!this.StopCommandReceived) {
try {this.Log.Add(EventLogEntryType.Information, "Executing job");// do workthis.Log.Add(EventLogEntryType.Information, "Finished job");
}catch (Exception x) {this.Log.Add(EventLogEntryType.Error, x);
}
Thread.Sleep(2000);
}
this.MainExecutionThread = null;
this.Log.Add(EventLogEntryType.Information, "Service stopped.");
}
It's up to you what to include in the log file, but I wanted to have some output and added more log entries than needed. The entire "processing" code is wrapped into a try-catch block. If the code we are going to implement throws an exception that we haven't handled elsewhere, it is written to the log and the service doesn't break.
Running the code in the Visual Studio environment now produces the following output in the output window:
'CrmOpportunityService.vshost.exe' (Managed): Loaded 'D:\BLOG\CRM 4.0 Samples\CrmOpportunityService\CrmOpportunityService\bin\Debug\CrmOpportunityService.exe', Symbols loaded.
'CrmOpportunityService.vshost.exe' (Managed): Loaded 'C:\Windows\assembly\GAC_MSIL\System.Configuration\2.0.0.0__b03f5f7f11d50a3a\System.Configuration.dll', Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Information: Service started.
Information: Executing job
Information: Finished job
Information: Executing job
Information: Finished job
Information: Executing job
Information: Finished job
Information: Stop command received.
The thread 0x1f8 has exited with code 0 (0x0).
The thread 0x1b7c has exited with code 0 (0x0).
Information: Service stopped.
The thread 0x1584 has exited with code 0 (0x0).
The thread 0x1b44 has exited with code 0 (0x0).
The program '[5068] CrmOpportunityService.vshost.exe: Managed' has exited with code 0 (0x0).
Again, it's a very simple log implementation, but you can see what commands are sent to the service, how it's working and behaving. Still not good enough, because I want to control the service the way it's used when deployed. So let's add our own service control manager application.
The Service Control Manager
It's a simple Windows form with four buttons on it for starting, stopping, pausing and resuming the service. I'm not explaining how to create a Windows Forms application. Instead I'm just showing the relevant code, which, in the beginning, only has implementations for the Start and Stop buttons:
public partial class ServiceControlManagerForm : Form, ILog {
CrmOpportunityService.CrmOpportunityService Service { get; set; }
StringBuilder NewLogEntries { get; set; }
object NewLogEntriesLock { get; set; }
public ServiceControlManagerForm() {
InitializeComponent();
this.btnStartService.Enabled = true;
this.btnStopService.Enabled = false;
this.btnPauseService.Enabled = false;
this.btnContinueService.Enabled = false;
this.NewLogEntries = new StringBuilder();
this.NewLogEntriesLock = new object();
}
private void btnStartService_Click(object sender, EventArgs e) {
if (this.Service == null) {
this.btnStartService.Enabled = false;
this.btnStopService.Enabled = true;
this.btnPauseService.Enabled = true;
this.btnContinueService.Enabled = false;
this.Service = new CrmOpportunityService.CrmOpportunityService(this);
this.Service.StartService();
}
}
private void btnStopService_Click(object sender, EventArgs e) {
if (this.Service != null) {
this.btnStartService.Enabled = true;
this.btnStopService.Enabled = false;
this.btnPauseService.Enabled = false;
this.btnContinueService.Enabled = false;
this.Service.Stop();
this.Service = null;
}
}
private void logTimer_Tick(object sender, EventArgs e) {
if (this.NewLogEntries.Length > 0) {
lock (this.NewLogEntriesLock) {
this.txtLog.Text = this.txtLog.Text + this.NewLogEntries.ToString();
this.txtLog.SelectionStart = this.txtLog.Text.Length - 1;
this.txtLog.SelectionLength = 0;
this.txtLog.ScrollToCaret();
this.NewLogEntries = new StringBuilder();
}
}
}
private void AddLogEntry(string message) {
lock (this.NewLogEntriesLock) {
this.NewLogEntries.AppendLine(message);
}
}
#region ILog Members
public void Add(System.Diagnostics.EventLogEntryType type, string message) {
this.AddLogEntry(type.ToString() + ": " + message);
}
public void Add(System.Diagnostics.EventLogEntryType type, string message, params object[] args) {
this.AddLogEntry(type.ToString() + ": " + string.Format(message, args));
}
public void Add(System.Diagnostics.EventLogEntryType type, Exception x) {
if (x is SoapException) {
SoapException e = x as SoapException;
string details = (e.Detail != null) ? e.Detail.InnerXml : "none";
this.AddLogEntry(type.ToString() + ": " + e.Message + "\r\nDetails: " + details);
}
this.AddLogEntry(x.ToString());
}
#endregion
}
The event handlers for the start and stop buttons should be self-explanatory. The important thing is the implementation of the ILog interface, allowing the form to serve as a log for our service implementation. To make it available in the service class, I added a second constructor and modified the first implementation:
public CrmOpportunityService()
: this(new VsDebugLog()) {
}
public CrmOpportunityService(ILog log) {
InitializeComponent();
this.Log = log;
}
Besides the four buttons on the form, there's also a timer control raising the Tick event every 250ms. You cannot access a Windows form from another thread and as the log messages are generated from the MainExecutionThread in our service class, it's not possible to simply append the messages to the text field, which is used to display them. Instead, I'm using a StringBuilder object and add the new messages to it. The logTimer_Tick method checks for new messages, adds them to the text box and removes them from the buffer. The lock statements are used to synchronize this process.
Running the form now allows starting and stopping the service and it also shows the log messages:
Supporting Pause and Continue
Now let's add pause and continue to the code. All we have to do is overriding the OnPause and OnContinue methods in the service class and add appropriate handling to our main execution thread:
namespace CrmOpportunityService {
public partial class CrmOpportunityService : ServiceBase {
...
bool Paused { get; set; }
...
protected override void OnPause() {this.Log.Add(EventLogEntryType.Information, "Pause command received.");this.Paused = true;
}protected override void OnContinue() {this.Log.Add(EventLogEntryType.Information, "Continue command received.");this.Paused = false;
}public void PauseService() {this.OnPause();
}public void ContinueService() {this.OnContinue();
}
private void Run() {
this.StopCommandReceived = false;
this.Paused = false;
this.Log.Add(EventLogEntryType.Information, "Service started.");
while (!this.StopCommandReceived) {
if (!this.Paused) {
try {
this.Log.Add(EventLogEntryType.Information, "Executing job");
// do work
this.Log.Add(EventLogEntryType.Information, "Finished job");
}
catch (Exception x) {
this.Log.Add(EventLogEntryType.Error, x);
}
}
Thread.Sleep(2000);
}
this.MainExecutionThread = null;
this.Log.Add(EventLogEntryType.Information, "Service stopped.");
}
}
}
OnPause and OnContinue simply set a variable to either true or false and the Run method stops its execution when entering the pause state. PauseService and ContinueService are helper methods, allowing us to execute the Pause and Continue commands from our own Service Control Manager application. The last step is adding appropriate code to the Pause and Continue buttons in the Service Control Manager:
private void btnPauseService_Click(object sender, EventArgs e) {
if (this.Service != null) {
this.btnStartService.Enabled = false;
this.btnStopService.Enabled = true;
this.btnPauseService.Enabled = false;
this.btnContinueService.Enabled = true;
this.Service.PauseService();
}
}
private void btnContinueService_Click(object sender, EventArgs e) {
if (this.Service != null) {
this.btnStartService.Enabled = false;
this.btnStopService.Enabled = true;
this.btnPauseService.Enabled = true;
this.btnContinueService.Enabled = false;
this.Service.ContinueService();
}
}
To let Windows know that our service supports Pause and Continue, open the service class in the design view and set the CanPauseAndContinue property to true:
Implementing the CRM code
We can now start, pause, continue and stop our service and do the CRM implementation. The service is intended to run on a CRM server machine (though you can run it on any machine) and as any CRM 4.0 server has the CRM SDK assemblies installed in the GAC, I'm using them in the service project instead of adding a web reference. However, you can also add a reference to the CRM web services and use it instead of the SDK assemblies.
I'm entirely separating the CRM code from the service code to make the service framework more reusable:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
namespace CrmOpportunityService {
public class CrmOpportunityExport {
ILog Log { get; set; }
public CrmOpportunityExport(ILog log) {
this.Log = log;
}
public void Execute() {
}
}
}
The only modification to the service code is in the Run method:
private void Run() {
this.StopCommandReceived = false;
this.Paused = false;
this.Log.Add(EventLogEntryType.Information, "Service started.");
while (!this.StopCommandReceived) {
if (!this.Paused) {
try {
this.Log.Add(EventLogEntryType.Information, "Executing job");
CrmOpportunityExport export = new CrmOpportunityExport(this.Log);
export.Execute();
this.Log.Add(EventLogEntryType.Information, "Finished job");
}
catch (Exception x) {
this.Log.Add(EventLogEntryType.Error, x);
}
}
Thread.Sleep(2000);
}
this.MainExecutionThread = null;
this.Log.Add(EventLogEntryType.Information, "Service stopped.");
}
The CrmOpportunityExport class of course needs access to the log file, which is the reason why I'm passing the log instance in the constructor. The Execute is quite simple, but I'm going to explain some helper methods in more detail:
public void Execute() {
DateTime now = DateTime.Now;
CrmService service = new CrmService();
service.Url = this.Configuration.ServerUrl + "/2007/crmservice.asmx";
service.UseDefaultCredentials = true;
service.CrmAuthenticationTokenValue = new CrmAuthenticationToken();
service.CrmAuthenticationTokenValue.AuthenticationType = 0;
service.CrmAuthenticationTokenValue.OrganizationName = this.Configuration.Organization;
QueryExpression query = new QueryExpression(EntityName.opportunity.ToString());
query.ColumnSet.AddColumns("estimatedclosedate", "estimatedvalue", "name", "opportunityid");
query.Criteria.AddCondition("modifiedon", ConditionOperator.LessEqual, now.ToString("s"));
if (this.Configuration.LastRun > DateTime.MinValue) {
query.Criteria.AddCondition("modifiedon", ConditionOperator.GreaterThan, this.Configuration.LastRun.ToString("s"));
}
BusinessEntityCollection opportunities = service.RetrieveMultiple(query);
if (opportunities.BusinessEntities.Count == 0) {
this.Log.Add(EventLogEntryType.Information, "No new or updated opportunities since " + this.Configuration.LastRun);
}
else {
this.Log.Add(EventLogEntryType.Information, opportunities.BusinessEntities.Count + " new or updated opportunities since " + this.Configuration.LastRun);
string fileContent = this.BuildExportFileContent(opportunities);
this.WriteExportFile(fileContent);
this.Configuration.LastRun = now;
}
}
The code retrieves all opportunities modified since the last export and this information has to be persisted. The easiest way to configure a service is the registry and I created a helper class reading the configuration values:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Win32;
using System.Configuration;
using System.Diagnostics;
namespace CrmOpportunityService {
public class ServiceConfiguration {
public const string ServiceRegistryRoot = @"Software\Stunnware\Samples\CrmOpportunityExport";
private DateTime _lastRun;
public string ServerUrl { get; private set; }
public string Organization { get; private set; }
public string ExportDirectory { get; private set; }
public int TimeoutBetweenRunsInSeconds { get; private set; }
public ServiceConfiguration(ILog log) {
RegistryKey serviceRoot = Registry.LocalMachine.OpenSubKey(ServiceRegistryRoot);
if (serviceRoot == null) {
throw new ConfigurationErrorsException("The HKLM\\" + ServiceRegistryRoot + " key does not exist in the registry.");
}
this.ServerUrl = this.ReadNonEmptyStringValue(serviceRoot, "ServerUrl");
this.Organization = this.ReadNonEmptyStringValue(serviceRoot, "Organization");
this.ExportDirectory = this.ReadNonEmptyStringValue(serviceRoot, "ExportDirectory");
object lastRun = serviceRoot.GetValue("LastRun");
if (lastRun != null) {
long ticks = long.Parse(lastRun.ToString());
this.LastRun = new DateTime(ticks);
}
else {
this.LastRun = DateTime.MinValue;
log.Add(EventLogEntryType.Warning, "The LastRun value does not exist in the HKLM\\" + ServiceConfiguration.ServiceRegistryRoot + " key of the registry. Exporting all opportunities.");
}
object timeoutBetweenRunsInSeconds = serviceRoot.GetValue("TimeoutBetweenRunsInSeconds");
if (timeoutBetweenRunsInSeconds != null) {
this.TimeoutBetweenRunsInSeconds = int.Parse(timeoutBetweenRunsInSeconds.ToString());
}
else {
this.TimeoutBetweenRunsInSeconds = 600;
log.Add(EventLogEntryType.Warning, "The TimeoutBetweenRunsInSeconds value does not exist in the HKLM\\" + ServiceConfiguration.ServiceRegistryRoot + " key of the registry. Setting to " + this.TimeoutBetweenRunsInSeconds + " seconds.");
}
}
private string ReadNonEmptyStringValue(RegistryKey serviceRoot, string name) {
string value = (string)serviceRoot.GetValue(name);
if (string.IsNullOrEmpty(value)) {
throw new ConfigurationErrorsException("The " + name + " value does not exist in the HKLM\\" + ServiceRegistryRoot + " key of the registry.");
}
return value;
}
public DateTime LastRun {
get {
return this._lastRun;
}
set {
this._lastRun = value;
RegistryKey serviceRoot = Registry.LocalMachine.CreateSubKey(ServiceRegistryRoot);
serviceRoot.SetValue("LastRun", value.Ticks);
}
}
}
}
Remember that the code will run on a server machine without any UI. Good error messages are a live saver when it comes to troubleshooting, so add as much information as possible. If a customer reads the Windows Event Log and finds a message that the CRM organization isn't configured and you also tell exactly where to add this information, then they may be able to solve the problem themselves. If you don't provide this information, then you most likely get a support call.
The ServiceConfiguration class introduces a new setting, the TimeoutBetweenRunsInSeconds. It is used in the main execution thread and controls how long the thread sleeps after having done it's job:
private void Run() {
this.StopCommandReceived = false;
this.Paused = false;
this.Log.Add(EventLogEntryType.Information, "Service started.");
ServiceConfiguration configuration = null;
try {
configuration = new ServiceConfiguration(this.Log);
}
catch (Exception x) {
this.Log.Add(EventLogEntryType.Error, x);
}
if (configuration != null) {
int remainingSeconds = 0;
while (!this.StopCommandReceived) {
Thread.Sleep(1000);
remainingSeconds--;
if (!this.Paused && (remainingSeconds <= 0)) {
try {
this.Log.Add(EventLogEntryType.Information, "Executing job");
CrmOpportunityExport export = new CrmOpportunityExport(this.Log);
export.Execute();
this.Log.Add(EventLogEntryType.Information, "Finished job");
}
catch (Exception x) {
this.Log.Add(EventLogEntryType.Error, x);
}
remainingSeconds = configuration.TimeoutBetweenRunsInSeconds;
}
}
}
this.MainExecutionThread = null;
this.Log.Add(EventLogEntryType.Information, "Service stopped.");
}
The previous implementation suspended the thread for 2000ms, which is acceptable when responding to a stop command. But say the service is configured to wait for 10 minutes after it has done its job, then stopping the service may also take up to 10 minutes, because the check for StopCommandReceived is executed after the sleep phase. To prevent such things, the thread is sleeping for one second and after that decrements the remainingSeconds variable. If it's down to 0, the job is executed and remainingSeconds is reset to the value configured in the registry.
The above implementation will instantly start the job, which, in most of the cases, is what you want. But it's just initializing remainingSeconds with configuration.TimeoutBetweenRunsInSeconds instead of setting it to 0 to wait in the beginning as well. You can easily add a new registry flag to even make it configurable.
All that's left is the export file generation, which is a very basic implementation:
private void WriteExportFile(string content) {
string filename = "ex" + DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss") + ".csv";
string path = Path.Combine(this.Configuration.ExportDirectory, filename);
FileInfo file = new FileInfo(path);
if (!file.Directory.Exists) {
file.Directory.Create();
}
using (FileStream fs = file.OpenWrite()) {
using (StreamWriter w = new StreamWriter(fs, Encoding.UTF8)) {
w.Write(content);
this.Log.Add(EventLogEntryType.Information, "Export file written to " + file.FullName);
}
}
}
private string BuildExportFileContent(BusinessEntityCollection opportunities) {
StringBuilder fileContent = new StringBuilder();
fileContent.AppendLine("opportunityid;name;estimatedclosedate;estimatedvalue");
foreach (opportunity o in opportunities.BusinessEntities) {
string estimatedCloseDate = (o.estimatedclosedate == null) ? null : o.estimatedclosedate.UniversalTime.ToString("u");
decimal estimatedValue = (o.estimatedvalue == null) ? 0 : o.estimatedvalue.Value;
fileContent.AppendLine(o.opportunityid.Value + ";" + o.name + ";" + estimatedCloseDate + ";" + estimatedValue);
}
return fileContent.ToString();
}
To test the code in development mode, we first run our Service Control Manager without configuring any values in the registry. This is just a test to see if our error handling is correct:
And the final test is adding the values to the registry:
After the values have been configured, the service should work in your development environment:
Supporting the Windows Event Log
Now it's time to plan for production. Our own Service Control Manager is great to test the service, but when running as a service, we cannot log to a Windows forms application. We have to implement Windows Event Logging, but you will see that it's damned easy:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Web.Services.Protocols;
using System.Net;
namespace CrmOpportunityService {
public class WindowsEventLog : ILog {
EventLog Log { get; set; }
public WindowsEventLog(EventLog log) {
this.Log = log;
}
#region ILog Members
public void Add(System.Diagnostics.EventLogEntryType type, string message) {
this.Log.WriteEntry(message, type);
}
public void Add(System.Diagnostics.EventLogEntryType type, string message, params object[] args) {
this.Log.WriteEntry(string.Format(message, args), type);
}
public void Add(System.Diagnostics.EventLogEntryType type, Exception x) {
if (x is SoapException) {
SoapException e = x as SoapException;
string details = (e.Detail != null) ? e.Detail.InnerXml : "none";
this.Log.WriteEntry(e.Message + "\r\nDetails: " + details, type);
}
}
#endregion
}
}
It's almost the same implementation as used in the other logs. The EventLog parameter is a member of each service class and we have to do a last change to our service implementation to run as a service:
public CrmOpportunityService() {
InitializeComponent();
this.Log = new WindowsEventLog(this.EventLog);
}
public CrmOpportunityService(ILog log) {
InitializeComponent();
this.Log = log;
}
When started as a service, the first constructor will be executed and we use the Windows Event Log to log our messages. When called from our own Service Control Manager application or using the VsDebugLog class, then the second constructor is used and we can look at the output without looking at the event log and hitting the refresh button every few seconds.
Installing the Service
Now it's time for the final test: running the application as a service. In order to do that, we have to install the service first. Open the designer view of the service class and right-click onto it:
The "Add Installer" option is what we need to configure our service and it's also available as a hyperlink in the properties window. Two components are added to our service: serviceInstaller1 and serviceProcessInstaller. The service installer contains most properties we need and I have set some typical values:
The service process installer defines the account the service is using. The default of "User" is what we need, so I haven't changed anything.
You can change this value to let the service run as the Local Service account, Local System account or Network Service account. However, we need a valid CRM user to successfully retrieve data from our CRM system and that means a user account.
Remember the conditional compilation symbol in the program.cs file? You have to un-define the RUN_IN_VS symbol to execute the ServiceBase.Run method instead of our own startup code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading;
namespace CrmOpportunityService {
static class Program {
/// <summary>
/// The main entry point for the application.
/// </summary>
static void Main() {
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[]
{
new CrmOpportunityService()
};
#if RUN_IN_VS
CrmOpportunityService service = new CrmOpportunityService();
service.StartService();
Thread.Sleep(5000);
service.Stop();
#else
ServiceBase.Run(ServicesToRun);
#endif
}
}
}
I only added the RUN_IN_VS symbol to the debug configuration, so all I had to do was switching to the release configuration.
Installing the service is pretty easy. The .NET Framework SDK has a utility named "installutil". It installs or uninstalls a service by either specifying the /i or the /u command line parameter. To register the release build as a service, start a command prompt and navigate to the directory containing the installutil.exe executable, which will be C:\Windows\Microsoft.NET\Framework64\v2.0.50727 or something similar (Framework64 if it's a 64-bit machine, otherwise just Framework). If you have Visual Studio installed, then simply run a Visual Studio Command Prompt and the PATH variable is set accordingly.
In the command prompt type installutil /i "full path to the service executable", e.g.
installutil /i "D:\BLOG\CRM 4.0 Samples\CrmOpportunityService\CrmOpportunityService\bin\Release\CrmOpportunityService.exe"
When inside a Visual Studio Command Prompt, you can also navigate to the directory containing the service executable and simply type
installutil /i CrmOpportunityService.exe
The specified parameters are shown in the console window and the installation starts:
>installutil /i CrmOpportunityService.exe
Microsoft (R) .NET Framework Installation utility Version 2.0.50727.3053
Copyright (c) Microsoft Corporation. All rights reserved.
Running a transacted installation.
Beginning the Install phase of the installation.
See the contents of the log file for the D:\BLOG\CRM 4.0 Samples\CrmOpportunityService\CrmOpportunityService\bin\Release\CrmOpportunityService.exe assembly's progress.
The file is located at D:\BLOG\CRM 4.0 Samples\CrmOpportunityService\CrmOpportunityService\bin\Release\CrmOpportunityService.InstallLog.
Installing assembly 'D:\BLOG\CRM 4.0 Samples\CrmOpportunityService\CrmOpportunityService\bin\Release\CrmOpportunityService.exe'.
Affected parameters are:
logtoconsole =
assemblypath = D:\BLOG\CRM 4.0 Samples\CrmOpportunityService\CrmOpportunityService\bin\Release\CrmOpportunityService.exe
i =
logfile = D:\BLOG\CRM 4.0 Samples\CrmOpportunityService\CrmOpportunityService\bin\Release\CrmOpportunityService.InstallLog
At this point a dialog pops up asking to provide the user credentials used to run the service:
And finally you get two more lines in the console window:
The Commit phase completed successfully.
The transacted install has completed.
That's it. You should now see the service in the Services window:
Start the service and open the event log to see the service event messages:
The Final Test
Our service is running now, but it doesn't mean it's working. To test, simply create a new opportunity or change an existing one and you should see a new export file in the export directory:
Because of the csv extension, a double-click opens the file in Microsoft Excel (if installed):
That's it. The entire source code is available for download and I hope that it helps you writing your own services in the future. Even if they are not related to CRM, because the majority of the code can be used for any service implementation