Tuesday, 3 July 2012

Continuous Integration with CRM Dynamics 2011

Lately my work colleague Thomas Swann described a bunch of tools that would make the life of the Crm Dynamics 2011 developer much easier. In this blog post I will show a practical example of how we can use them to automate the build & deployment and enable continuous integration in our Dynamics project.

We will create a MSBuild task that packages a CRM solution source into a single package (using Solution Packager) and then deploys it to CRM server using CRM SDK. The example will also allow to revert the entire process i.e. export a solution package from CRM and unpack it.

CRM access layer

Let's start by creating a layer for communicating with CRM using SDK. You will need to reference microsoft.xrm.sdk.dll and microsoft.xrm.sdk.proxy.dll assemblies to make the code compile.
/// 
/// Class used to establish a connection to CRM
/// 
public class CrmConnection
{
    private Uri _organizationUrl;
    private ClientCredentials _credentials;
    private OrganizationServiceProxy _service;

    public CrmConnection(string organizationUrl, string username, string password)
    {
        _credentials = new ClientCredentials();
        _credentials.UserName.UserName = username;
        _credentials.UserName.Password = password;

        this._organizationUrl = new Uri(organizationUrl);
    }

    public IOrganizationService Service
    {
        get
        {
            if (_service == null)
            {
                _service = new OrganizationServiceProxy(
                                     _organizationUrl, null, _credentials, null);
                _service.ServiceConfiguration.CurrentServiceEndpoint
                                    .Behaviors.Add(new ProxyTypesBehavior());
               _service.Authenticate();
            }
            return _service;
        }
    }
}

/// 
/// CRM Solution Manager for performing solution operations
/// 
public class SolutionManager
{
    IOrganizationService _service;

    public SolutionManager(IOrganizationService service)
    {
        _service = service;
    }

    /// 
    /// Imports a solution to CRM server
    /// 
    /// Path to solution package
    public void ImportSolution(string zipPath)
    {
        byte[] data = File.ReadAllBytes(zipPath);
 
        ImportSolutionRequest request = 
                  new ImportSolutionRequest() { CustomizationFile = data };

        Console.WriteLine("Solution deploy started...");
        _service.Execute(request);
        Console.WriteLine("Solution deployed");
    } 

    /// 
    /// Exports a solution package from CRM and saves it at specified location
    /// 
    /// Name of the solution to be exported
    /// Path to save the exported package at
    public void ExportSolution(string solutionName, string zipPath)
    {
        ExportSolutionRequest request = new ExportSolutionRequest()
        {
            SolutionName = solutionName,
            Managed = false
        };

        Console.WriteLine("Solution export started...");

        ExportSolutionResponse response = 
                               (ExportSolutionResponse)_service.Execute(request);
        File.WriteAllBytes(zipPath, response.ExportSolutionFile);

        Console.WriteLine("Solution successfully exported");
    }
}
This gives us a CRM access layer that we can use in our code (not only in msbuild task code). It allows us to import and export packages from CRM and save them to disk at specified location.

Custom MsBuild task

Now it's time to create custom MSBuild tasks that would utilize the SolutionManager described above. Let's start by introducing a common base for CRM tasks:
/// 
/// Base class for CRM tasks, including all details required to connect to CRM
/// 
public abstract class CrmSolutionTask : Microsoft.Build.Utilities.Task
{
    [Required]
    public string OrganisationUrl { get; set; }

    [Required]
    public string Username { get; set; }

    [Required]
    public string Password { get; set; }

    [Required]
    public string ZipPath { get; set; }

    protected SolutionManager SolutionManager
    {
        get 
        {
            CrmConnection connection = 
                          new CrmConnection(OrganisationUrl, Username, Password); 
            return new SolutionManager(connection.Service);
        }
    }
}
All the public properties from that class will be available as task parameters and are common for both tasks. Now let's create the Import tasks:
public class ImportSolutionTask : CrmSolutionTask
{
    public override bool Execute()
    {
        try
        {
             this.SolutionManager.ImportSolution(ZipPath);
        }
        catch (Exception e)
        {
            Log.LogError("Exception while importing CRM solution: " + e);
            return false;
        }
        return true;
    }
}
The ExportSolutionTask is actually very similar. Note that it defines an additional public property, which will also be used as task parameter, specific to that task only.
public class ExportSolutionTask : CrmSolutionTask
{
    [Required]    
    public string SolutionName { get; set; }

    public override bool Execute()
    {
        try
        {
             this.SolutionManager.ExportSolution(SolutionName, ZipPath); 
        }
        catch (Exception e)
        {
            Log.LogError("Exception while exporting CRM solution: " + e);
            return false;
        }
        return true;
    }
}

MsBuild script

Now that we have our custom build tasks coded let's make use of them in the MsBuild script. The following build script will be stored in CRM.build file and assumes we keep our custom tasks in "BuildTasks" project.

  
  
  
  
  
    
    
    SERVER_NAME
    
    
    SERVER_NAME
    
    ORGANIZATION_URL
    CRM_ADMIN_USERNAME
    CRM_ADMIN_PASSWORD

    CRM_SOLUTION_NAME
    $(MSBuildProjectDirectory)\CrmSolutions
    $(CrmSolutionsFolder)\$(CrmSolutionName).zip

    
    $(MSBuildProjectDirectory)\Tools
    $(ToolsFolder)\SolutionPackager.exe

    Debug    
  
  
  
  
    
      
    
    
    
    
    
  

  
  
  
    
    
  
  
    
    
  

  
  
  
    
    
  
  
    
    
  
  
  
  
    
  
  
    
  

To pack and deploy a solution to CRM run the "DeploySolution" target:
msbuild CRM.build /t:DeploySolution
To download and extract CRM solution run the "DownloadSolution" target:
msbuild CRM.build /t:DownloadSolution
Note that you can override all MsBuild properties from the command line when running that script.

Continuous Integration

We are closely coming to the end of this post and you have probably noticed that, despite the post title, I haven't mentioned the CI part yet. Well, how do you use described MSBuild tasks in your CI process is really up to you and your project needs. In my current project we are storing an extracted solution in our code repository. Our CI server is configured to run the "DeploySolution" target after each change to that source i.e. the code is packed and imported to our CRM test server. This assures that the CRM test server always uses the latest version of that solution.

Developers who work on the solution on their own CRM instances can use the "DownloadSolution" target to automatically obtain the updated solution package and extract it, so they don't have to do that manually.

Our Import and Export tasks can also be used to automate the process of moving solutions between environments.

2 comments:

Marco said...

Hi,
What great post! It really can make the CRM Dynamics 2011 developer much easier.

Did you post the whole project in somewhere?
Thanks in advance.

TheCaughtException said...

Flip - I've created an MSBUILD script that uses an ITask borrowing heavily from above. My VS build has some C# plugins that I deploy via the MS Deploy target. I Export the resulting CRM Solution as Managed using stuff based on your above example MSBuild Task.

I'm thinking that my Export is somehow hosed, or similar, as the resulting Solution's plugins do not execute correctly.

I get an error

System.TypeLoadException: Could not load type 'System.Runtime.CompilerServices.ExtensionAttribute' from assembly 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'

- which leads me to think I'm missing something in my assembled-Solution.

If I do the Deploy from within Visual Studio, then Export from within CRM, all is well.

Any thoughts or ideas greatly appreciated.