Accessing other assemblies from CRM .net dlls

5 minute read time.

The Problem:

Sometimes you may want to share code across your custom CRM pages that are generated from seperate assemblies. For example you may want to define a Web class for a list page of Custom Entity A and use it in your app factory for Custom Entity B (I'm assuming that both your entites are in seperate assemblies, something that I'd highly recommend if you are not already doing this). It makes sense to keep all the code for Entity A in the same place. However this sometimes poses a problem because .net does not always know where your assemblies are at run-time.

If this happens you will get an error like the following in the CRM dotnet logs:

Apr 6 2010 17:48:58.238 5704 7 5 Could not load file or assembly 'JacksAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=511df46c58e89be3' or one of its dependencies. The system cannot find the file specified.

In this blog post I will be exploring an elegant way of solving this problem. (Well I think it's elegant... I'd love to hear what you think in the comments section below!)

How to get .net to find your assembly

Microsoft has an article on this that describes 3 methods for resolving this problem:

Method 1: Install the assembly in the global assembly cache (GAC)

This method doesn't work for me for a couple of reasons:

  1. I cannot easily debug an assembly that is in the GAC because Visual Studio will not allow you to step into a registered assembly.
  2. Having to register an assembly in the GAC complicates the installation of my CRM component.

Method 2: Use an application configuration (.config) file with the tags

This method doesn't work because CRM does not use .config files.

Method 3: Use the AssemblyResolve event

This method will work, but we have to solve a few minor problems. My programming style is very non-invasive; I don't like to hard code things that cannot be reliably determined at compile time and I do not like to have to put code into my objects that is unrelated to what the object is for. So this with this style in mind method 3 immediately poses two problems for me:

  1. How can I make sure the event handler is attached without having to add duplicate lines of code to each of my AppFactory methods?
  2. How can I reliably determine the path to my assembly at run-time?

Global code for AppFactory methods

To solve the first problem I am going to use a neat feature in C# called a static constructor. If we add a static constructor to the AppFactory class it will get called everytime CRM calls one of the AppFactory methods, before they execute. (Note this works because CRM completely unloads the AppDomain after every call to a .net assembly).

        static AppFactory()
        {
            // This code will get executed before each call to an AppFactory method
            ...
        }

So that solves the problem of where to put the event hooking code, now onto the next problem:

Method to determine CRM Installation path at run-time

The CRM .net API makes this very easy by providing a method called MetaData.GetParam which will allow us to query any system parameter, and there just so happens to be a system parameter called InstallDirName that tells us exactly what we want to know. However the MetaData.GetParam method is only available within Sage.CRM.WebObject.Web classes. This poses a new problem: how do we get access to it from within the static method event handler?

Method to gain access to CRM Web object methods from any point in your code

Enter a handy little static class I call Stub. This class simply provides a Web object just for API calls. It is a lazy singleton; it only gets instantiated once and only when needed.

    public static class Stub
    {
        private class WebToe : Sage.CRM.WebObject.Web
        {
            public WebToe() { }
            public override void BuildContents() { throw new NotImplementedException(); }
        }
 
        private static WebToe web;
        public static Sage.CRM.WebObject.Web Web
        {
            get
            {
                if (web == null) web = new WebToe();
                return web;
            }
        }
    }

If you include this class in your project you can easily access CRM web API calls from any point simply by using this syntax:

Stub.Web.Metadata.GetParam("InstallDirName")

Note: Use Stub conservatively, this is one of the few examples where this kind of usage is a good idea, if you are using it ask yourself: should I really be creating a new class that inherits from one of the web classes instead?

Putting it all together

OK so what does that all look like in the end? Here is an example base.cs file for you to see:

using Sage.CRM.WebObject;
using System;
using System.Reflection;
 
namespace MyNameSpace
{
    public static class Stub
    {
        private class WebToe : Sage.CRM.WebObject.Web
        {
            public WebToe() { }
            public override void BuildContents() { throw new NotImplementedException(); }
        }
 
        private static WebToe web;
        public static Sage.CRM.WebObject.Web Web
        {
            get
            {
                if (web == null) web = new WebToe();
                return web;
            }
        }
    }
 
    public static class AppFactory
    {
        static AppFactory()
        {
            // This code will get executed before each call to an AppFactory method
            AppDomain currentDomain = AppDomain.CurrentDomain;
            currentDomain.AssemblyResolve += new ResolveEventHandler(CRMResolveEventHandler);
        }
 
        private static Assembly CRMResolveEventHandler(object sender, ResolveEventArgs args)
        {
            //This handler is called only when the common language runtime tries to bind to the assembly and fails.
 
            //Retrieve the list of referenced assemblies in an array of AssemblyName.
            Assembly MyAssembly, objExecutingAssemblies;
            string strTempAssmbPath = "";
 
            objExecutingAssemblies = Assembly.GetExecutingAssembly();
            AssemblyName[] arrReferencedAssmbNames = objExecutingAssemblies.GetReferencedAssemblies();
 
            //Loop through the array of referenced assembly names.
            foreach (AssemblyName strAssmbName in arrReferencedAssmbNames)
            {
                //Check for the assembly names that have raised the "AssemblyResolve" event.
                if (strAssmbName.FullName.Substring(0, strAssmbName.FullName.IndexOf(",")) == args.Name.Substring(0, args.Name.IndexOf(",")))
                {
                    //Build the path of the assembly from where it has to be loaded.
                    strTempAssmbPath = Stub.Web.Metadata.GetParam("InstallDirName") + "\\CustomDotNet\\" + args.Name.Substring(0, args.Name.IndexOf(",")) + ".dll";
                    break;
                }
            }
            //Load the assembly from the specified path. 
            MyAssembly = Assembly.LoadFrom(strTempAssmbPath);
 
            //Return the loaded assembly.
            return MyAssembly;
        }
 
        public static void RunMyPage(ref Web AretVal)
        {
            AretVal = new MyPage();
        }
    }
}

Note: You must not reference unregistered assemblies in the AppFactory constructor. This is because .net could complain before it gets a chance to load the handler. Make sure you do not use any referenced assemblies until after the event handler has been loaded.

Hopefully this blog post has taught you a few new tricks. Please do let me know how useful / not useful this was to you in the comment section below.

Kind regards,
Jack

Parents
  • I'm late to this party but here is a quick and dirty way without using the Stub. This assumes the calling dll is in the same location as the to-resolve dll.

    [code]

    // If you project uses additional DLLs, they will be resolved here.

    static AppFactory() => AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CRMResolveEventHandler);

    ///

    /// This handler is called only when the common language runtime tries to bind to the assembly and fails.

    ///

    private static Assembly CRMResolveEventHandler(object sender, ResolveEventArgs args)

    {

    var thisDLLName = Assembly.GetExecutingAssembly().GetName().Name;

    var thisDllLocation = Assembly.GetExecutingAssembly().Location;

    var toResolveDllName = args.Name.Substring(0, args.Name.IndexOf(","));

    var resolvedLocation = thisDllLocation.Replace(thisDLLName, toResolveDllName);

    return Assembly.LoadFrom(resolvedLocation);

    }

    [/code]

Comment
  • I'm late to this party but here is a quick and dirty way without using the Stub. This assumes the calling dll is in the same location as the to-resolve dll.

    [code]

    // If you project uses additional DLLs, they will be resolved here.

    static AppFactory() => AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CRMResolveEventHandler);

    ///

    /// This handler is called only when the common language runtime tries to bind to the assembly and fails.

    ///

    private static Assembly CRMResolveEventHandler(object sender, ResolveEventArgs args)

    {

    var thisDLLName = Assembly.GetExecutingAssembly().GetName().Name;

    var thisDllLocation = Assembly.GetExecutingAssembly().Location;

    var toResolveDllName = args.Name.Substring(0, args.Name.IndexOf(","));

    var resolvedLocation = thisDllLocation.Replace(thisDLLName, toResolveDllName);

    return Assembly.LoadFrom(resolvedLocation);

    }

    [/code]

Children
No Data