A while ago I posted a sample on how to develop a Windows Service targeting Microsoft Dynamics CRM. I used my own sample to create a service that is doing something meaningful. You probably know about Joris Kalz' Caching Tool for Microsoft Dynamics CRM 3.0 and I asked him if he's planning to upgrade it to a version targeting CRM 4.0. As there were no plans, I decided to create it myself and share it with you. It's really simple and comes with the entire code, so you can change it to your needs.
If you don't know what I'm talking about, then here's the short explanation: an ASP.NET application isn't loaded before accessed for the first time. And an ASP.NET application is unloaded if the IIS application pool is recycled. As CRM is an ASP.NET application, you should have noticed that there is a delay when opening the CRM web client for the first time, because usually the application pool was recycled and CRM isn't initialized. The time needed for the first initialization is very different from system to system and ranges between a few seconds up to 30 seconds and sometimes even more. This problem isn't in any way specific to CRM.
What has to be initialized?
When making the first request to CRM (or any other ASP.NET page), then IIS loads the requested page and parses it. If ASP.NET code is associated with the page, then it is compiled and executed. If one or more assemblies are bound to the page, then they have to be loaded. Some assemblies have references to other assemblies and they are loaded as well. Web service bindings may be initialized and finally the code is executed, which often is an initial caching of metadata and/or reading of configuration values. This takes some seconds and it's extremely annoying for a user to wait until it's done. The goal of Joris' and my implementation simply is a background service polling CRM - or other web applications - continuously. If the IIS application pool is recycled or the ASP.NET application has to be initialized again, then the service will do it and the user doesn't have to wait when starting the application.
CRM uses the SQL Server as its database backend and SQL Server uses caching as well. The more traffic you have on a SQL database, the more gets cached and the faster the system becomes. At least in theory. So accessing all kind of CRM entities builds up the SQL cache as well. All of it is done by the Keep Alive Service.
Contents of the configuration file
The application only consists of two files: the service executable and the configuration file. The configuration file has to be placed into the same directory where you put the executable and is has the same filename as the executable plus the ".txt" extension. By default the service executable is named Stunnware.Crm4.KeepAliveService.exe and the configuration file is named Stunnware.Crm4.KeepAliveService.exe.txt.
The configuration file is really easy, but powerful enough to automatically ping every CRM view or a subset of views. It also allows the use of manually defined URLs, so you are not limited to CRM URLs at all. I'm using the tool myself to keep the Filtered Lookup application alive:
# Stunnware Keep Alive Service URL List
#
# Every
non-empty line that is not a comment (starting with the hash symbol) is
requested.
# After each request the service waits for 10 seconds before
requesting the next URL. You can
# override this setting in the registry
by specifying the following:
#
#
[HKEY_LOCAL_MACHINE\SOFTWARE\Stunnware\KeepAliveService]
# "TimeoutBetweenRunsInSeconds"=dword:00000005
#
# The value of TimeoutBetweenRunsInSeconds sets the new timeout (5
seconds in the above example).
#
# The service writes errors to the
Windows Application Event Log. If you want to make sure that the
# URLs
are requested, add the following registry value:
#
#
[HKEY_LOCAL_MACHINE\SOFTWARE\Stunnware\KeepAliveService]
# "EventLogIncludeRequestedUrls"=dword:00000001
#
# Restart the service to let it pickup the new setting. It then writes
an informational entry for each
# URL to the event log.
SERVER =
SW01:5555, SWPROD, SWDEV
SERVER = SWDC:5555, Stunnware
URL =
http://$SERVER/$ORG/isv/stunnware.com/cld4/cld4.aspx?orgname=$ORG
URL
=
http://$SERVER/$ORG/isv/stunnware.com/cld4/lookupsingle.aspx?orgname=$ORG&class=SwRetrieveMultipleAll
URL =
http://$SERVER/$ORG/isv/stunnware.com/cld4/keepalive.aspx?orgname=$ORG
URL =
http://$SERVER/$ORG/_root/homepage.aspx?etc=$VIEW-ETC&viewid=$VIEW-ID
VIEWTYPES = MainApplicationView
Obviously the hash sign is used for comments and they explain what parameters you can set in the registry to specify advanced configuration values. The only real configuration values are SERVER, URL and VIEWTYPES. You can specify as much SERVER entries as you want, though you usually want to have only one. Each SERVER entry defines the server name and port plus the organizations (organization unique name) to ping. In the above sample, you see two servers, SW01:5555 and SWDC:5555. On SW01:5555 the SWPROD and SWDEV organizations should be processed, while on SWDC:5555 only the Stunnware organization should be used.
$SERVER and $ORG are replaced with the server and organization name at runtime, so http://$SERVER/$ORG/isv/stunnware.com/cld4/cld4.aspx?orgname=$ORG becomes, for instance, http://SW01:5555/SWDEV/isv/stunnware.com/cld4/cld4.aspx?orgname=SWDEV.
The URL lines specify which URLs to ping. You can use any URL you want, so feel free to add other web-based applications if they profit from the service as well. The first three lines are used to initialize the Filtered Lookup. The fourth URL is somewhat special:
http://$SERVER/$ORG/_root/homepage.aspx?etc=$VIEW-ETC&viewid=$VIEW-ID
If a URL contains both the $VIEW-ETC and the $VIEW-ID placeholders, then it is duplicated for each view matching the criteria of VIEWTYPES. VIEWTYPES contains one or more values of the following enumeration:
namespace KeepAliveService {
/// <summary>
/// Valid query types used by CRM. This enumeration is used in the configuration file to specify which views
/// are processed automatically, without specifying the view URLs manually.
/// </summary>
public enum QueryTypes {
/// <summary>
/// Specifies an address book filter.
/// </summary>
AddressBookFilters = 512,
/// <summary>
/// Specifies an advanced search.
/// </summary>
AdvancedSearch = 1,
/// <summary>
/// Specifies a lookup view.
/// </summary>
LookupView = 64,
/// <summary>
/// Specifies the main application view.
/// </summary>
MainApplicationView = 0,
/// <summary>
/// Specifies the main application view without a subject.
/// </summary>
MainApplicationViewWithoutSubject = 1024,
/// <summary>
/// Specifies the marketing automation query.
/// </summary>
MaSearch = 32,
/// <summary>
/// Specifies an offline Microsoft Dynamics CRM for Microsoft Office Outlook filter query.
/// </summary>
OfflineFilters = 16,
/// <summary>
/// Specifies a Microsoft Dynamics CRM for Outlook filter query.
/// </summary>
OutlookFilters = 256,
/// <summary>
/// Specifies a quick find query.
/// </summary>
QuickfindSearch = 4,
/// <summary>
/// Specifies a reporting query.
/// </summary>
Reporting = 8,
/// <summary>
/// Specifies a saved query.
/// </summary>
SavedQueryTypeOther = 2048,
/// <summary>
/// Specifies the service management appointment book view.
/// </summary>
SmAppointmentbookView = 128,
/// <summary>
/// Specifies a sub-grid.
/// </summary>
SubGrid = 2,
}
}
Separate multiple values with commas.
Installation
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 "C:\Program Files\Stunnware\Keep Alive Service\Stunnware.Crm4.KeepAliveService.exe"
When inside a Visual Studio Command Prompt, you can also navigate to the directory containing the service executable and simply type
installutil /i Stunnware.Crm4.KeepAliveService.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 C:\Program Files\Stunnware\Keep Alive
Service\Stunnware.Crm4.KeepAliveService.exe assembly's progress.
The file
is located at C:\Program Files\Stunnware\Keep Alive
Service\Stunnware.Crm4.KeepAliveService.InstallLog.
Installing assembly
'C:\Program Files\Stunnware\Keep Alive
Service\Stunnware.Crm4.KeepAliveService.exe'.
Affected parameters are:
logtoconsole =
assemblypath = C:\Program Files\Stunnware\Keep Alive
Service\Stunnware.Crm4.KeepAliveService.exe
i =
logfile = C:\Program
Files\Stunnware\Keep Alive Service\Stunnware.Crm4.KeepAliveService.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:

Update 07.03.2009
A problem was reported that the program crashes when a query does not include any Fetch XML and this observation is true. The following fix, highlighted in bold, should prevent it:
private void CreateUrlList() {
this.UrlsToPing = new List<string>();
foreach (CrmServerConfiguration serverConfig in this.Servers) {
foreach (string organization in serverConfig.Organizations) {
foreach (string crmServerUrl in this.Urls) {
string url = crmServerUrl.Replace("$SERVER", serverConfig.ServerNameAndPort).Replace("$ORG", organization);
int pViewEtc = url.IndexOf("$VIEW-ETC");
int pViewId = url.IndexOf("$VIEW-ID");
if ((pViewEtc == -1) && (pViewId == -1)) {
this.UrlsToPing.Add(url);
}
else {
//The following setup assumes that you can access the CRM server with the http protocol and that AD authentication is used.
//This should be the correct setup for almost every installation where this service runs. However, feel free to change the
//implementation if you need a different connection method.
CrmService crmService = new CrmService();
crmService.Url = "http://" + serverConfig.ServerNameAndPort + "/mscrmservices/2007/crmservice.asmx";
crmService.UseDefaultCredentials = true;
crmService.CrmAuthenticationTokenValue = new Microsoft.Crm.Sdk.CrmAuthenticationToken();
crmService.CrmAuthenticationTokenValue.OrganizationName = organization;
//Retrieve the system views using a FetchXML query.
XmlDocument doc = new XmlDocument();
doc.LoadXml(crmService.Fetch(Resource1.AllQueries));
XmlNodeList nodes = doc.SelectNodes("resultset/result");
foreach (XmlNode node in nodes) {
XmlNode fetchXmlNode = node.SelectSingleNode("fetchxml");
if (fetchXmlNode != null) {
string fetchXml = fetchXmlNode.InnerText;
string etc = node.SelectSingleNode("returnedtypecode").InnerText;
string id = node.SelectSingleNode("savedqueryid").InnerText;
int queryType = int.Parse(node.SelectSingleNode("querytype").InnerText);
QueryTypes type = (QueryTypes)queryType;
//Check if the view type was specified in the configuration. If so, then build the view URL and add it to the URL list.
if (this.ViewTypes.Contains(type)) {
string newUrl = url.Replace("$VIEW-ETC", etc).Replace("$VIEW-ID", id);
this.UrlsToPing.Add(newUrl);
}
}
}
}
}
}
}
}
The download was updated accordingly.