Fork me on GitHub

Technology behind the scenes

In the previous blog post How to Daxif, plug-in synchronization we merely described the reason we created this functionality as well as how to use it.

In this post we will go more in detail on how we handle the synchronization. In some solutions we end up with around +10 mb assembly files that we would only like to upload to the solution (cloud can be slow) if it’s strictly necessary.

Synchronization steps

1) Hash of the assembly

We start by getting the hash of the assembly we are building. You might think that can’t be that difficult right? Well it’s not, but you need to have in mind that you can’t just make an SHA-1 checksum om the builted binary as due to non-deterministic compiler optimizations, you will not end up with the same bits everytime. Therefore we inspired ourselves in Gustavo Guerra - ovatsus Setup.fsx which we modified so it will retrieve all the files and binaries related to the project itself as well as all the related projects. The final hash is just the combination of all the other hashes (fold):

1
2
3
4
5
let proj' = Path.GetFullPath(proj)
let hash =
    projDependencies proj' |> Set.ofSeq
    |> Set.map(fun x -> File.ReadAllBytes(x) |> sha1CheckSum')
    |> Set.fold(fun a x -> a + x |> sha1CheckSum) String.Empty
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// Used to retrieve a .vsproj dependencies (recursive)
let projDependencies (vsproj:string) = 
  let getElemName name =
    XName.Get(name, "http://schemas.microsoft.com/developer/msbuild/2003")
      
  let getElemValue name (parent : XElement) = 
    let elem = parent.Element(getElemName name)
    if elem = null || String.IsNullOrEmpty elem.Value then None
    else Some(elem.Value)
      
  let getAttrValue name (elem : XElement) = 
    let attr = elem.Attribute(XName.Get name)
    if attr = null || String.IsNullOrEmpty attr.Value then None
    else Some(attr.Value)
      
  let (|??) (option1 : 'a Option) option2 = 
    if option1.IsSome then option1
    else option2

  let fullpath path1 path2 = Path.GetFullPath(Path.Combine(path1, path2))

  let rec projDependencies' vsproj' = seq {
    let vsProjXml = XDocument.Load(uri = vsproj')

    let path = Path.GetDirectoryName(vsproj')

    let projRefs = 
      vsProjXml.Document.Descendants(getElemName "ProjectReference")
      |> Seq.choose (fun elem -> getAttrValue "Include" elem)
      |> Seq.map(fun elem -> fullpath path elem)

    let refs = 
      vsProjXml.Document.Descendants(getElemName "Reference")
      |> Seq.choose (fun elem ->
        getElemValue "HintPath" elem |?? getAttrValue "Include" elem)
      |> Seq.filter (fun ref -> ref.EndsWith(".dll"))
      |> Seq.map(fun elem -> fullpath path elem)
      
    let files = 
      vsProjXml.Document.Descendants(getElemName "Compile")
      |> Seq.choose (fun elem -> getAttrValue "Include" elem)
      |> Seq.map(fun elem -> fullpath path elem)
      
    for projRef in projRefs do
      yield! projDependencies' projRef
    yield! refs
    yield! files }

  projDependencies' vsproj

Note: As related binaries, which are retrieved from NuGet, always have the same bits, we can just SHA-1 checksum them as well.

2) Upload the assembly if none

In order to do the synchronization we will need to have an assembly to compare to. Therefore, the next thing we do is to ensure that there actually is an assembly in MS CRM. It’s important to store the calculated hash value from the previous step pa.Attributes.Add(“sourcehash”, hash) as it will be the value we will compare the local assembly in order to decide if we need to upload a newer version of the assembly.

3) Parse plug-in Steps and Types from local assembly

As mentioned in the previous blog post, we rely on the Plugin base class that is part of the Developer Toolkit, but we have expanded it (several times) so we can parse the neccesary information to register events on the solution:

For more information on usage, please read the following: Plugin Registration Setup.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class AccountPostPlugin : Plugin
{
    public AccountPostPlugin()
        : base(typeof(AccountPostPlugin))
    {
        RegisterPluginStep<AnyEntity>(
            EventOperation.Associate, 
            ExecutionStage.PostOperation, 
            ExecuteAccountPostPlugin);

        RegisterPluginStep<AnyEntity>(
            EventOperation.Disassociate, 
            ExecutionStage.PostOperation, 
            ExecuteAccountPostPlugin);

        RegisterPluginStep<Account>(
            EventOperation.Update, 
            ExecutionStage.PostOperation, 
            ExecuteAccountPostPlugin);
    }

    ...
  }

The local assembly is reflected so we can parse out the desired information:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let typesAndMessages (asm:Assembly) =
  asm.GetTypes() |> fun xs -> 
    let y = xs |> Array.filter (fun x -> x.Name = @"Plugin") |> Array.toList
                |> List.head
    xs
    |> Array.filter (fun (x:Type) -> x.IsSubclassOf(y))
    |> Array.Parallel.map (fun (x:Type) -> 
      Activator.CreateInstance(x), x.GetMethod(@"PluginProcessingStepConfigs"))
    |> Array.Parallel.map (fun (x, (y:MethodInfo)) -> 
        y.Invoke(x, [||]) :?> 
          ((string * int * string * string) * 
            (int * int * string * int * string * string) * 
              seq<(string * string * int * string)>) seq)
    |> Array.toSeq
    |> Seq.concat
    |> Seq.map(fun x -> tupleToRecord x)

let tupleToRecord ((a,b,c,d),(e,f,g,h,i,j),images) = 
  let step = 
    { className = a; executionStage = b; eventOperation = c;
      logicalName = d; deployment = e; executionMode = f;
      name = g; executionOrder = h; filteredAttributes = i; 
      userContext = Guid.Parse(j)}
  let images' =
    images
    |> Seq.map( fun (j,k,l,m) ->
      { name = j; entityAlias = k;
        imageType = l; attributes = m; } )
  { step = step; images = images' } 

6) Validation

Because we reflect from assemblies, we add some validation to ensure correctness:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let validateAssociateDisassosiate =
  associateDisassociateNoFilters
  >> bind associateDisassociateNoImages
  >> bind associateDisassociateAllEntity

let validate client =
  postOperationNoAsync
  >> bind preOperationNoPreImages
  >> bind validateAssociateDisassosiate
  >> bind preEventsNoPreImages
  >> bind postEventsNoPostImages
  >> bind (validUserContext client)

let validatePlugins plugins client =
  plugins
  |> Seq.map(fun pl -> ((messageName pl.step),pl))
  |> validate client

5) Now we can sync but …

This is the tricky part, but, it can easily be read from the following function (composed with several other functions):

1
2
3
4
5
6
7
8
9
let syncPlugins x = 

  deletePluginImages x
  >> deletePluginSteps x
  >> deletePluginTypes x
  >> updateAssembly x
  >> syncTypes x
  >> syncSteps x
  >> syncImages x

So the way MS CRM plug-in registration work is that in order to remove a plug-in you must first remove all of it’s types (subclasses in C#). Before you can remove a type, you will have to remove all of the steps (Create, Update, …) from each type. And finally, before you can remove a step, you will have to remove all of the images (Pre or Post) from all of the steps.

Once this is done, you can now upload a newer version of the assembly and then you would go the other way around. Create the types, then the steps and finally the images.

Note: I will not go into deeper code details here as the blog post would become to large. Besides, the code is Open Source, so feel free to look at It if you find the topic interesting.

Summary

By having this approach, everyting related to plug-in are stored as code, we can ensure that who ever retrieves the latest built from the source control. Will be able to synchronize the desired state of the MS CRM kernel expansion that is implemented for a given solution. This is normally the approach you have when you make other kind of software solutions. Therefore I must go back to our motto: “We don’t make MS CRM solutions, but software solutions”.

Kudos to our M.Sc. students

As we tend to code Daxif based on computer science principles, we would really like to thank DTU Compute, Department of Applied Mathematics and Computer Science for having provided us with three very skilled students that currently maintain and expands the tool with new features as Microsoft add new functionallity to MS CRM. We are also thankful for them to choose to stay with us when they finish their education.

More info:

comments powered by Disqus