Inside Microsoft Dynamics CRM 3.0

Arne Janning

  Home  |   Contact  |   Syndication    |   Login
  3 Posts | 0 Stories | 12 Comments | 36 Trackbacks

News

Archives

MSCRM blogs I read

Monday, November 07, 2005 #

Hi. My name is Arne Janning, I live in Heidelberg, Germany, and I am a programmer. This Blog is about programming, extending, hacking, deploying and integrating Microsoft Dynamics CRM 3.0.

Nuff said!

Saturday, January 28, 2006 #

So... I didn't post for a long time, I have been on holidays since new year, didn't have an internet connection for a long time - I actually write this from an internet cafe.

In my last post I promised to show how to get inside CRM and do customizations that go far beyond of what is supported. I also already mentioned the main entry point of the whole CRM-web-application, which is the Microsoft.Crm.MainApplication.Application_OnStart()-method in Microsoft.Crm.Application.Pages.dll. Microsoft.Crm.MainApplication is referenced in the global.asax-file in the CRM-webroot:

<%@ Application language="c#" Inherits="Microsoft.Crm.MainApplication" CodeBehind="Microsoft.Crm.Application.Pages.dll"%>

<%@ Application language="c#" Inherits="Microsoft.Crm.MainApplication" CodeBehind="Microsoft.Crm.Application.Pages.dll"%>

<%@ Application language="c#" Inherits="Microsoft.Crm.MainApplication" CodeBehind="Microsoft.Crm.Application.Pages.dll"%>

<%@ Application language="c#" Inherits="Microsoft.Crm.MainApplication.Application" CodeBehind="Microsoft.Crm.Application.Pages.dll"%>

We will write our own CRM-host to get inside CRM and get access to the internal CRM-object model at runtime. This is fairly easy.

First of all, create class library project in Visual Studio .NET 2003 - it must be .NET 1.1. In my example the project has the name "Janning.Crm.Host".

Then make sure you have added the following references:

  • System.dll
  • System.Data.dll
  • System.XML.dll
  • System.Drawing.dll
  • System.Web.dll
  • Microsoft.Crm.dll (from the GAC)
  • Microsoft.Crm.Application.Components.Application.dll (from \bin)
  • Microsoft.Crm.Application.Components.Core.dll (from \bin)
  • Microsoft.Crm.Application.Components.Platform.dll (from \bin)
  • Microsoft.Crm.Platform.Sdk.dll (from the GAC)

There is a small trick to copy assemblies from the GAC : Press Start --> Run and then enter C:\Windows\assembly\gac. The shell extension which is normally running on the assembly-folder won't show up then and you can easily copy and paste the CRM-assemblies and reference them from VS.NET.

Then you have to add the following code into a codefile:

using System;
using System.Collections;
using System.ComponentModel;
using System.Configuration;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Reflection;
using System.Security.Principal;
using System.Web;
using System.Web.SessionState;
using System.Xml;

using Microsoft.Crm;
using Microsoft.Crm.Application.Platform;
using Microsoft.Crm.Errors;
using Microsoft.Crm.Metadata;
using Microsoft.Crm.Security;
using Microsoft.Crm.Utility;

namespace Janning.Crm.Host
{
 public class CustomCrmApplicationHost : HttpApplication
 {
  protected void Application_Start(Object sender, EventArgs e)
  {
   RegControl.LoadLibraries();
   NotificationManager.ExtraParameter = HttpContext.Current;
   MetadataCacheConfig.LoadMethod = LoadMethod.Database;
   NotificationManager.StartNotificationsThread(new Notification());
  }

  protected void Application_AuthenticateRequest(Object sender, EventArgs e)
  {
   try
   {    
    UserCache.GetCurrentUser();
   }
   catch (Exception ex)
   {
    COMException comex = ex as COMException;
    string errorCode = "0xffffffff";
    if (comex != null)
    {
     errorCode = comex.ErrorCode.ToString("x", CultureInfo.InvariantCulture);
    }
    string redirectPath = string.Format(
     CultureInfo.InvariantCulture,
     "/_common/error/authenticationError.htm?0x{0}&{1}",
     errorCode,
     base.Request.Url.AbsoluteUri);
    base.Response.Redirect(redirectPath);
    base.Response.End();
    CrmTrace.TraceFormat(
     TraceCategory.Application,
     TraceLevel.Error,
     "An error occurred during the Application_OnAuthenticateRequest : \nError: {0} \nStack Trace:{1}",
     ex.Message,
     ex.StackTrace);
   }
  }

  protected void Application_Error(Object sender, EventArgs e)
  {
   base.Response.Clear();
   Exception ex = base.Server.GetLastError();
   ErrorInformation info = new ErrorInformation(ex, base.Request.Url);
   if (ConfigurationSettings.AppSettings["DevErrors"] != "On")
   {
    info.StackTrace = string.Empty;
   }
   if (info.Source == "XML")
   {
    base.Response.ContentType = "text/xml";
    ErrorInformation.XmlSerializer.Serialize(base.Response.OutputStream, info);
    base.Response.End();
   }
   else if (info.Source == "SOAP")
   {
    base.Response.Clear();
    base.Response.ContentType = "text/xml";
    base.Response.StatusCode = 500;
    XmlTextWriter writer = new XmlTextWriter(base.Response.Output);
    writer.WriteStartDocument();
    writer.WriteStartElement("soap", "Envelope", "
http://schemas.xmlsoap.org/soap/envelope/");
    writer.WriteStartElement("Body", "
http://schemas.xmlsoap.org/soap/envelope/");
    writer.WriteStartElement("Fault", "
http://schemas.xmlsoap.org/soap/envelope/");
    writer.WriteElementString("faultcode", "Server");
    writer.WriteElementString("faultstring", info.Description);
    writer.WriteStartElement("detail");
    ErrorInformation.XmlSerializer.Serialize(writer, info);
    writer.WriteEndDocument();
    base.Response.End();
   }
   else
   {
    HttpException httpEx = ex as HttpException;
    string maxRequestLenghText = "Maximum request length exceeded.";
    string pathFileName = Path.GetFileName(base.Request.Path);
    if ((pathFileName == "print_data.aspx") || (base.Request.Path == "/crmreports/download.aspx"))
    {
     base.Response.ClearHeaders();
    }
    if ((pathFileName == "importFieldMap.aspx") && (httpEx.ErrorCode == -2147467259) && ((ex.InnerException != null) ? (maxRequestLenghText == ex.InnerException.Message) : true))
    {
     base.Server.ClearError();
     base.Response.Redirect("/Tools/BulkImport/importFileChoose.aspx?errorMessage=BulkImport_Error_Exceed_MaxFileSize");
     base.Response.End();
    }
    else if ((pathFileName == "upload.aspx") && (httpEx.ErrorCode == -2147467259))
    {
     base.Server.ClearError();
     base.Response.Redirect("/_common/error/uploadFailure.aspx?hr=0x80043e08");
     base.Response.End();
    }
    else if (pathFileName == "print_data.aspx")
    {
     base.Server.ClearError();
     base.Response.Redirect("/_common/error/popuperror.aspx?hr=" +
      httpEx.ErrorCode.ToString());
     base.Response.End();
    }
    else if (ConfigurationSettings.AppSettings["DevErrors"] == "On")
    {
     //left this out for the moment
    }
    else
    {
     base.Response.Redirect(
      "/_common/error/errorhandler.aspx?errNum=" +
      HttpUtility.UrlEncode(info.Code) +
      "&errMessage=" +
      HttpUtility.UrlEncode(info.Description));
     base.Response.End();
    }
   }
  }
 }
}

Compile the code into an assembly, copy the assembly into the \bin-folder, make a backup of the global.asax-file and change the global.asax-file to this:

<%@ Application language="c#" Inherits="Janning.Crm.Host.CustomCrmApplicationHost" CodeBehind="Janning.Crm.Host.dll"%>

<%@ Application language="c#" Inherits="Janning.Crm.Host.CustomCrmApplicationHost" CodeBehind="Janning.Crm.Host.dll"%>

<%@ Application language="c#" Inherits="Janning.Crm.Host.CustomCrmApplicationHost" CodeBehind="Janning.Crm.Host.dll"%>

<%@ Application language="c#" Inherits="Janning.Crm.Host.CustomCrmApplicationHost" CodeBehind="Janning.Crm.Host.dll"%>

(If your assembly-name or namespace is different you have to change this of course)

Make an iisreset (Start --> Run --> iisreset) and open CRM again: http://yourCrmServerOrIP

Everything just works exactly like it did before, there is only one difference: if you use a tool like Process Explorer and look which dlls are loaded into the w3wp.exe-process you'll see that the Janning.Crm.Host.dll is actually running in the process. As the code in this assembly which is running in the most central part of CRM is now completely under your control you can do pretty much anything - if you know the internal object model. I'll write about this soon in detail and give you a couple of examples.

I should mention again that all this is of course completely unsupported by Microsoft, me or my future employer.

If someone wants to have the complete VS.NET-solution-files then just leave a comment here and I'll send it via email. I have no access to an FTP-server at the moment so I can't offer a download - perhaps someone has some empty space for me? :-)

(And please, could somebody explain me how to use the font sizes in .Text-Admin - I simply don't get it)


Wednesday, November 16, 2005 #

So let's simply start this blog.

One of my goals is to show how one can inject custom code in CRM 3.0 - I'm not talking about callouts or supported and documented stuff (of course I will write about this as well on this blog) - I'm talking about changing the internal objects and data structures at runtime.

To that end we will have to look a lot at the code Reflector gives us, to see what's really going on under the hood. To that same end - and for many other things - using the built-in tracing-mechanism is really useful to see what MSCRM actually does.

Although there is a section "Registry Settings" in the SDK it is not documented how to enable tracing.

But Reflector shows that there is a class called CrmTrace in Microsoft.Crm.dll. The method Microsoft.Crm.CrmTrace.LoadTrace() : Boolean shows the registry keys that are necessary to enable tracing:

private static bool LoadTrace()
{
      string[] textArray2;
     //[...]
     textArray2 = new string[] { "TraceEnabled", "TraceSchedule", "TraceCallStack", "TraceCategories" } ;
     //[...]
     text1 = "TraceEnabled";
     CrmTrace.isTracingOff = ((int) RegistryCache.GetValue("TraceEnabled")) == 0;
     //[...]

 

So to enable the tracing-mechanism you simply have to add some registry keys to HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MSCRM:

  • TraceEnabled (dword) - set the value to 1 to turn on tracing, set the value to 0 to turn it off again
  • TraceDirectory (string) - this is the directory where the trace files are stored. The directory has to exist, CRM will not create the directory.
  • TraceCategories (string) - set the value to *:Verbose
  • TraceCallStack (dword) - set the value to 1 if your're interested in the stack trace
  • TraceRefresh (dword) - set the value to 1
  • TraceSchedule (string) - set it to one of the values of the Microsoft.Crm.TraceSchedule-enum: e.g. Daily or Hourly

It is not necessary to restart IIS, MSCRM 3.0 has a mechanism of getting notified of configuration-changes at runtime. This mechanism gets started by the Microsoft.Crm.MainApplication.Application_OnStart()-method in Microsoft.Crm.Application.Pages.dll:

NotificationManager.StartNotificationsThread(new Notification());

Microsoft.Crm.MainApplication.Application_OnStart() is the main entrypoint for the whole MSCRM-application. We will speak about this later in much greater detail.

To be continued...