Search

A generic HTTP Module for Microsoft Dynamics CRM

When reading the CRM newsgroups you often find questions like "How can I make JavaScript functions available in all forms" or "Is it possible to change the color of a row in a view". There are two types of answers:
  1. Not in any supported way.
  2. Use an HTTP module.
While answer number 2 is unsupported as well, it is a good alternative. However beside the answer "Use an HTTP module" I never saw an example or any hints on how to do it..

What is an HTTP module? 

An HTTP module is an extension for IIS and it's general purpose is to hook into the stream between IIS and the client to log or modify data. A request to a file on the web server is first served by IIS. It loads the file and if it contains server code (ASP, ASP.NET), the code is executed. Once that's done, the document is sent to the client.
If you install an HTTP module, it is invoked on each request, so after all of the server side processing is done, the response stream is passed to all HTTP modules. After all of them have finished, the final result is passed to the client. Each HTTP module can change the content of the stream, so let's start to build a module allowing us to include additional JavaScript include files and links to cascading style sheets.

Implementing the IHttpModule interface

Creating an HTTP module is very easy. All you have to do is to implement the System.Web.IHttpModule interface. It only has two methods, Init and Dispose. Unless you have something to dispose, you only need to care about the Init method. It is called once and serves as the general initialization routine. You use the context parameter to add event handlers and they are invoked whenever a request is made. That's said an HTTP module must be fast, otherwise it may slow down your server dramatically.

Here's the implementation I'm using is this sample:

/// <summary>
/// The HttpModule implementation.
/// </summary>
public class HttpModule : IHttpModule {
public HttpModule() {
}
/// <summary>
/// The Init method is called from IIS. It is used to add handlers for the events we're interested in.
/// </summary>
/// <param name="context"></param>
public void Init(HttpApplication context) {
context.BeginRequest += new EventHandler(OnBeginRequest);
}
/// <summary>
/// A resource is requested.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnBeginRequest(object sender, EventArgs e) {
HttpContext context = ((HttpApplication) sender).Context;
string queryString = context.Request.QueryString.ToString();
if (queryString.Length == 0) {
Log.Write(context.Request.Path);
}
else {
Log.Write(context.Request.Path + "?" + queryString);
}
if (context.Request.Path.ToLower().EndsWith(".aspx")) {
//We only parse the content of files with the aspx extension. This prevents us
//from checking image files, style sheets or SOAP documents.
context.Response.Filter = new CrmFilter(context.Response.Filter, context.Request);
}
}
public void Dispose() {
//we don't have disposable objects but need to implement this method (part
//of the IHttpModule interface)
}
}

Our only subscription is BeginRequest (our OnBeginRequest event handler). Once applied, the OnBeginRequest method is called for every single file requested by a client, containing images, JavaScript files, style sheets, HTML controls and of course ASP.NET pages. We all know that CRM forms are ASP.NET web pages and they all have an extension of ".aspx". Only these must be evaluated and therefore we only apply a filter for .aspx pages.


What is a filter?

A filter (the context.Response.Filter) is a Stream. It is not a standard stream, like a FileStream or a MemoryStream. It is a class deriving from the abstract Stream class. If a filter is applied, the stream operations are passed to the filter instead of using the response stream directly.

Here's the interesting part of the CrmFilter class:

public class CrmFilter : Stream {
const string ReqSpace = @"\s+";
const string Argument = @"(\w+)";
const string Quote = "\\\"";
//RegEx pattern to search the form object type
const string Pattern = "<input" + ReqSpace + "type=" + Quote + "hidden" + Quote + ReqSpace + "id=" + Quote + "crmFormSubmitObjectTypeName" + Quote + ReqSpace + "value=" + Quote + Argument + Quote + ">";
StringBuilder _buffer = new StringBuilder();
Stream _stream;
bool _isHtmlFile;
HttpRequest _request;
/// <summary>
/// Initializes a new filter.
/// </summary>
/// <param name="stream">The response stream containing the original CRM page.</param>
/// <param name="request">The ASP.NET request object.</param>
public CrmFilter(Stream stream, HttpRequest request) {
_stream = stream;
_request = request;
}
/// <summary>
/// The main purpose of this Write implementation is to inject code into the response stream.
/// To know what to inject, we have to parse the HTML document.
/// </summary>
/// <param name="buffer">A chunk of data.</param>
/// <param name="offset">The offset of the data inside the CRM page.</param>
/// <param name="count">The number of bytes.</param>
public override void Write(byte[] buffer, int offset, int count) {
//If we already know that the content passed to the client is not a HTML document,
//we directly write the buffer to the client.
if ((offset > 0) && !_isHtmlFile) {
_stream.Write(buffer, offset, count);
return;
}
//To analyze the data, we have to copy it to a buffer first.
_buffer.Append(UTF8Encoding.UTF8.GetString(buffer, offset, count));
//Now convert it to an HTML string.
string html = _buffer.ToString();
//If this is the first data sent to the client, we're going to check if it is an
//HTML document. This simply is done by looking for the <html> tag. Though not
//totally correct, it has worked so far.
if (offset == 0) {
//Check for the <html> tag
Match match = new Regex("<html>", RegexOptions.IgnoreCase).Match(html);
//If the <html> tag was found, we will buffer the entire stream until we also
//found the </html> tag.
_isHtmlFile = match.Success;
}
//Again, if this is not an HTML document, send the buffer to the client.
if (!_isHtmlFile) {
_stream.Write(buffer, offset, count);
return;
}
//As an HTML document usually ends with the </html> tag, we assume that the entire
//document is loaded when we find it.
Match htmlMatch = new Regex("</html>", RegexOptions.IgnoreCase).Match(html);
//If we found the </html> tag
if (htmlMatch.Success) {
//Find the </head> element
Match headMatch = new Regex( "</head>", RegexOptions.IgnoreCase).Match(html);
if (headMatch.Success) {
//Find the entity type name. This will be set if it is a CRM form, but not in views.
Match entityMatch = new Regex(Pattern, RegexOptions.IgnoreCase).Match(html);
string entityTypeName = null;
if (entityMatch.Success && (entityMatch.Groups != null) && (entityMatch.Groups.Count > 0)) {
entityTypeName = entityMatch.Groups[0].ToString();
for(int i = 1; i < entityMatch.Groups.Count; i++) {
string text = entityMatch.Groups[i].ToString();
if (text.Length < entityTypeName.Length) {
entityTypeName = text;
}
}
}
if (entityTypeName != null) {
Log.Write("Found entity " + entityTypeName);
}
//Get the HTML to inject.
string includesHtml = Config.GetHead(entityTypeName);
if (includesHtml != null) {
//If there is something to inject, place it right before the </head> tag.
_buffer.Insert(headMatch.Index, includesHtml);
}
}
//Convert the html text to an UTF8-encoded byte array and send it to the client.
byte[] data = UTF8Encoding.UTF8.GetBytes(_buffer.ToString());
//That's it. The html code now contains additional javascript include files or style sheets.
_stream.Write(data, 0, data.Length);
}
}
}
view raw CrmFilter.cs hosted with ❤ by GitHub
Our only subscription is BeginRequest (our OnBeginRequest event handler). Once applied, the OnBeginRequest method is called for every single file requested by a client, containing images, JavaScript files, style sheets, HTML controls and of course ASP.NET pages. We all know that CRM forms are ASP.NET web pages and they all have an extension of ".aspx". Only these must be evaluated and therefore we only apply a filter for .aspx pages.

What to include?

The easiest implementation would be to include a single link to a JavaScript file in all ASP.NET pages. However I thought it would be better to differentiate a bit and added a very simple configuration file:
<?xml version="1.0" encoding="utf-8" ?>
<Config xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<All>
<Include href="/MyConfig/config/css/styles.css" type="css" />
</All>
<AllEntities>
<Include href="/MyConfig/config/scripts/global.js" type="js" />
</AllEntities>
<Entities>
<Entity name="account">
<Includes>
<Include href="/MyConfig/config/scripts/account.js" type="js" / -->
</Includes>
</Entity>
</Entities>
</Config>
view raw WebConfig.xml hosted with ❤ by GitHub
This configuration file must be placed in /MyConfig/config and must be named config.xml. However you can change that in the Config class:
string configFilePath = webSitePath + "MyConfig\\config\\config.xml";
The above configuration file defines the following: 
  • Include files listed in the All section are added to all HTML files. In the sample configuration above, the following line is injected into the head of the html document: <link rel="stylesheet" type="text/css" href="/MyConfig/config/css/styles.css">.
  • Include files listed in the AllEntities section are added to all CRM forms. That's the reason why I'm looking for the entity type name when parsing the HTML content. In the sample configuration above, the following line is injected into the head of the html document:<script language="javascript" src="/MyConfig/config/scripts/global.js" />
  • Finally, include files listed in the Entities section are added only if the HTML document is a CRM form of the specified type. In the sample configuration above, the following line is injected: <script language="javascript" src="/MyConfig/config/scripts/account.js" />.

You can add as many entries as you like. The xml structure simply is a serialized Config class and the "Current" accessor deserializes it. Feel free to change it to your needs. Also note that I have included some very basic logging. By default it logs to C:\CRMModule\log.txt and only when running the debug version. You can change this in the Log class.

How to install it

You have to place the assembly into two locations on your CRM server:
  1. /bin
  2. /mscrmservices/bin

If you have modified the web to contain additional bin directories, you may have to copy them in there too. As an alternative you can add the assembly to the GAC. For this reason I added a strong name to the assembly, as unsigned assemblies are not allowed in the GAC.
Finally, open the web.config in the CRM root and make the highlighted changes:

<system.web>
    ...
    <httpModules>
        <add name="MyConfig.Crm.HttpModule" type="MyConfig.Crm.HttpModule, MyConfig.Crm.HttpModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1ed2e80199a69f48"/>
    </httpModules>
</system.web>

Note that there may be two system.web sections, one for the main CRM application and one for the reporting services, which is identified by the following xml element:
<location path="Reports">
Be sure to add the module to the main CRM configuration and do not put it into the reports section.

And finally: How to test it

Create a new JavaScript file and add the following function:
function MyConfig_Test(message) {
    alert(message);
}

Save it at /MyConfig/config/scripts/global.js.

You can of course use any name for your function, but to not conflict with functions in other include files, I'm using a unique prefix. I leave it up to you to follow that or not.

Use the following configuration file (/MyConfig/config/config.xml):
<?xml version="1.0" encoding="utf-8" ?>
    <Config xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <All/>
        <AllEntities>
            <Include href="/MyConfig/config/scripts/global.js" type="js" />
        </AllEntities>
        <Entities/>
</Config>

Open a CRM form and add the following to your OnLoad event:

MyConfig_Test("It works!");

If it doesn't work, try resetting IIS. If it indeed does work and you come across some cool functions, I would love to see them to build some kind of function repository that everyone can download (of course the poster will get full credits for it).

Playing with the old CDO object

Here is an example that might work if the system supports CDO object
using System;
using CDO;
using ADODB;
namespace Client.Office_Integration
{
public class UsingCDOEx
{
static void Main(string[] args)
{
Message MyMessage = new MessageClass();
Configuration MyConfig = MyMessage.Configuration;
Fields MyFields = MyConfig.Fields;
MyFields[@"http://schemas.microsoft.com/cdo/configuration/sendusing"].Value = 2;
MyFields[@"http://schemas.microsoft.com/cdo/configuration/smtpserverport"].Value = 25;
MyFields[@"http://schemas.microsoft.com/cdo/configuration/smtpserver"].Value = "smarthost";
MyFields.Update();
MyMessage.Configuration = MyConfig;
MyMessage.TextBody = "This is a test message";
MyMessage.Subject = "Testing";
MyMessage.From = "billgates@microsoft.com";
MyMessage.To = "stevejob@apple.com";
MyMessage.Send();
}
}
}