Sticky

This blog has moved to www.dreamingincrm.com. Please update your feed Url. Thank you.

Showing posts with label plugin. Show all posts
Showing posts with label plugin. Show all posts

6 May 2015

Plugin Registration Tool and Plugin Profile

Having spent most of today trying out a failed plugin experiment, I discovered a feature of Plugin Registration tool that I never used before. It it not very clear from the SDK documentation, what keys are there in the InputParameters and OutputParameters and often I refer the appropriate request/response class, to find out what key I should be using on the InputParameter e.g. for UpdateRequest use Target key on the InputParameter for a pre-operation plugin.

There seems to be a easier way to capture the plugin state right after the plugin fires. This can be done using the Plugin Registration tool. This is the plugin command bar:


Replay Plug-in Execution button is used when you want to debug the plugin in Visual Studio. This process is well documented in multiple blogs and SDK documentation. View Plug-in profile functionality is what we want to use during the initial plugin development stage. After registering a plugin, right click on the desired step and choose Start Profiling.
Persist the profile to entity, instead of throwing an exception.

Once this is done, perform the action that triggers the plugin. This creates a profile record, that we can use to dump the plugin state. After the plugin has finished executing, click the view plugin profile button in the command bar. It brings up this window.

Next, click the downarrow button and choose the appropriate trace record.

After clicking the View button, you can see the entire plugin state. The below is the xml I got for a plugin firing on Retrieve message on SavedQuery.

<Profile>
  <Configuration i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" />
  <ConstructorDurationInMilliseconds>2</ConstructorDurationInMilliseconds>
  <ConstructorException i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" />
  <ConstructorStartTime>2015-05-06T01:46:45.5675566Z</ConstructorStartTime>
  <Context>
    <z:anyType i:type="PluginExecutionContext" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
      <BusinessUnitId>1f06daae-de79-e411-80bd-00155dc6799a</BusinessUnitId>
      <CorrelationId>09993a86-41c8-40b7-bc4c-2717057b5408</CorrelationId>
      <Depth>1</Depth>
      <InitiatingUserId>af20daae-de79-e411-80bd-00155dc6799a</InitiatingUserId>
      <InputParameters xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
        <a:KeyValuePairOfstringanyType>
          <b:key>Target</b:key>
          <b:value i:type="a:EntityReference">
            <a:Id>00000000-0000-0000-00aa-000010001001</a:Id>
            <a:LogicalName>savedquery</a:LogicalName>
            <a:Name i:nil="true" />
          </b:value>
        </a:KeyValuePairOfstringanyType>
        <a:KeyValuePairOfstringanyType>
          <b:key>ColumnSet</b:key>
          <b:value i:type="a:ColumnSet">
            <a:AllColumns>false</a:AllColumns>
            <a:Columns xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
              <c:string>fetchxml</c:string>
              <c:string>name</c:string>
              <c:string>description</c:string>
              <c:string>returnedtypecode</c:string>
              <c:string>layoutxml</c:string>
              <c:string>savedqueryid</c:string>
              <c:string>name</c:string>
              <c:string>statecode</c:string>
              <c:string>statuscode</c:string>
              <c:string>ismanaged</c:string>
              <c:string>iscustomizable</c:string>
            </a:Columns>
          </b:value>
        </a:KeyValuePairOfstringanyType>
        <a:KeyValuePairOfstringanyType>
          <b:key>ReturnNotifications</b:key>
          <b:value i:type="c:boolean" xmlns:c="http://www.w3.org/2001/XMLSchema">true</b:value>
        </a:KeyValuePairOfstringanyType>
        <a:KeyValuePairOfstringanyType>
          <b:key>RelatedEntitiesQuery</b:key>
          <b:value i:nil="true" />
        </a:KeyValuePairOfstringanyType>
      </InputParameters>
      <IsExecutingOffline>false</IsExecutingOffline>
      <IsInTransaction>false</IsInTransaction>
      <IsOfflinePlayback>false</IsOfflinePlayback>
      <IsolationMode>2</IsolationMode>
      <MessageName>Retrieve</MessageName>
      <Mode>0</Mode>
      <OperationCreatedOn>2015-05-06T01:46:45.5206856Z</OperationCreatedOn>
      <OperationId>00000000-0000-0000-0000-000000000000</OperationId>
      <OrganizationId>a240c2fd-16a0-4ab5-9ecb-a43a6afdcc8c</OrganizationId>
      <OrganizationName>Contoso</OrganizationName>
      <OutputParameters xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
        <a:KeyValuePairOfstringanyType>
          <b:key>BusinessEntity</b:key>
          <b:value i:type="a:Entity">
            <a:Attributes>
              <a:KeyValuePairOfstringanyType>
                <b:key>savedqueryid</b:key>
                <b:value i:type="z:guid">00000000-0000-0000-00aa-000010001001</b:value>
              </a:KeyValuePairOfstringanyType>
              <a:KeyValuePairOfstringanyType>
                <b:key>fetchxml</b:key>
                <b:value i:type="c:string" xmlns:c="http://www.w3.org/2001/XMLSchema"><fetch version="1.0" output-format="xml-platform" mapping="logical"><entity name="account"><attribute name="name" /><attribute name="address1_city" /><order attribute="name" descending="false" /><filter type="and"><condition attribute="ownerid" operator="eq-userid" /><condition attribute="statecode" operator="eq" value="0" /></filter><attribute name="primarycontactid" /><attribute name="telephone1" /><attribute name="accountid" /><link-entity alias="accountprimarycontactidcontactcontactid" name="contact" from="contactid" to="primarycontactid" link-type="outer" visible="false"><attribute name="emailaddress1" /></link-entity></entity></fetch></b:value>
              </a:KeyValuePairOfstringanyType>
              <a:KeyValuePairOfstringanyType>
                <b:key>returnedtypecode</b:key>
                <b:value i:type="c:string" xmlns:c="http://www.w3.org/2001/XMLSchema">account</b:value>
              </a:KeyValuePairOfstringanyType>
              <a:KeyValuePairOfstringanyType>
                <b:key>name</b:key>
                <b:value i:type="c:string" xmlns:c="http://www.w3.org/2001/XMLSchema">My Active Accounts</b:value>
              </a:KeyValuePairOfstringanyType>
              <a:KeyValuePairOfstringanyType>
                <b:key>statuscode</b:key>
                <b:value i:type="a:OptionSetValue">
                  <a:Value>1</a:Value>
                </b:value>
              </a:KeyValuePairOfstringanyType>
              <a:KeyValuePairOfstringanyType>
                <b:key>iscustomizable</b:key>
                <b:value i:type="a:BooleanManagedProperty">
                  <a:CanBeChanged>true</a:CanBeChanged>
                  <a:ManagedPropertyLogicalName>iscustomizableanddeletable</a:ManagedPropertyLogicalName>
                  <a:Value>true</a:Value>
                </b:value>
              </a:KeyValuePairOfstringanyType>
              <a:KeyValuePairOfstringanyType>
                <b:key>layoutxml</b:key>
                <b:value i:type="c:string" xmlns:c="http://www.w3.org/2001/XMLSchema"><grid name="resultset" object="1" jump="name" select="1" icon="1" preview="1"><row name="result" id="accountid"><cell name="name" width="300" /><cell name="telephone1" width="100" /><cell name="address1_city" width="100" /><cell name="primarycontactid" width="150" /><cell name="accountprimarycontactidcontactcontactid.emailaddress1" width="150" disableSorting="1" /></row></grid></b:value>
              </a:KeyValuePairOfstringanyType>
              <a:KeyValuePairOfstringanyType>
                <b:key>ismanaged</b:key>
                <b:value i:type="c:boolean" xmlns:c="http://www.w3.org/2001/XMLSchema">true</b:value>
              </a:KeyValuePairOfstringanyType>
              <a:KeyValuePairOfstringanyType>
                <b:key>statecode</b:key>
                <b:value i:type="a:OptionSetValue">
                  <a:Value>0</a:Value>
                </b:value>
              </a:KeyValuePairOfstringanyType>
            </a:Attributes>
            <a:EntityState i:nil="true" />
            <a:FormattedValues>
              <a:KeyValuePairOfstringstring>
                <b:key>returnedtypecode</b:key>
                <b:value>Account</b:value>
              </a:KeyValuePairOfstringstring>
              <a:KeyValuePairOfstringstring>
                <b:key>statuscode</b:key>
                <b:value>Active</b:value>
              </a:KeyValuePairOfstringstring>
              <a:KeyValuePairOfstringstring>
                <b:key>iscustomizable</b:key>
                <b:value>True</b:value>
              </a:KeyValuePairOfstringstring>
              <a:KeyValuePairOfstringstring>
                <b:key>ismanaged</b:key>
                <b:value>Managed</b:value>
              </a:KeyValuePairOfstringstring>
              <a:KeyValuePairOfstringstring>
                <b:key>statecode</b:key>
                <b:value>Active</b:value>
              </a:KeyValuePairOfstringstring>
            </a:FormattedValues>
            <a:Id>00000000-0000-0000-00aa-000010001001</a:Id>
            <a:LogicalName>savedquery</a:LogicalName>
            <a:RelatedEntities />
          </b:value>
        </a:KeyValuePairOfstringanyType>
      </OutputParameters>
      <OwningExtension xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts">
        <a:Id>517294bb-91f3-e411-80d0-00155dc6799a</a:Id>
        <a:LogicalName>sdkmessageprocessingstep</a:LogicalName>
        <a:Name>RYR.VirtualViews.SavedQueryOnRetrieveMultiplePlugin: Retrieve of savedquery (Profiler)</a:Name>
      </OwningExtension>
      <PostEntityImages xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
      <PreEntityImages xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
      <PrimaryEntityId>00000000-0000-0000-00aa-000010001001</PrimaryEntityId>
      <PrimaryEntityName>savedquery</PrimaryEntityName>
      <RequestId i:nil="true" />
      <SecondaryEntityName>none</SecondaryEntityName>
      <SharedVariables xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
      <UserId>af20daae-de79-e411-80bd-00155dc6799a</UserId>
      <ParentContext i:nil="true" />
      <Stage>40</Stage>
    </z:anyType>
  </Context>
  <ExecutionDurationInMilliseconds>8</ExecutionDurationInMilliseconds>
  <ExecutionException i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" />
  <ExecutionStartTime>2015-05-06T01:46:45.5675566Z</ExecutionStartTime>
  <HasServiceEndpointNotificationService>true</HasServiceEndpointNotificationService>
  <IsContextReplay>false</IsContextReplay>
  <IsolationMode>2</IsolationMode>
  <OperationType>Plugin</OperationType>
  <ProfileVersion>1.1</ProfileVersion>
  <ReplayEvents xmlns:a="http://schemas.datacontract.org/2004/07/PluginProfiler.Plugins" />
  <SecureConfiguration i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" />
  <TypeName>RYR.VirtualViews.SavedQueryOnRetrieveMultiplePlugin</TypeName>
  <WorkflowInputParameters xmlns:a="http://schemas.datacontract.org/2004/07/PluginProfiler.Plugins" />
  <WorkflowOutputParameters xmlns:a="http://schemas.datacontract.org/2004/07/PluginProfiler.Plugins" />
  <WorkflowStepId i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" />
</Profile>

As you can see, this file is quite useful in trying to understand the plugin execution context and can be beneficial in the initial development stage. This feature has been in PluginRegistration tool since CRM2011, and I felt like an idiot for having just discovered it, but happy that I finally discovered this. I will definitely be using this a lot in the coming days.

22 October 2014

Executing Quick Find from Console Application : Redux

If you have been following my blog, you might remember this (http://nycrmdev.blogspot.com.au/2014/05/executing-quickfind-using-crm-sdk.html) post about executing a quick find query from the console. It was using an undocumented message, and hence it is unsupported. I had a crack at this problem one more time, this time using Actions.

In order to return the quick find results, the custom action need to have a EntityCollection output parameter. I posted this (https://community.dynamics.com/crm/f/117/t/128534.aspx) question in CRM forums sometime back and didn't get any response.


This is not production ready code and just demonstrates how this can be done. If you would rather read the code, instead of this post please find the download link in the very bottom.

Requirements:
For the search - should be able to specify:
1.) Entity name
2.) Search term
3.) Page number
4.) Number of records to be returned

Step 1: Create a custom action

 
The body of the action is empty and doesn't contain any logic. The actual quickfind will be performed by a plugin registered post-operation of this action.

Step 2: Create the plugin

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Linq;
using System.Xml.Linq;
using Contract = System.Diagnostics.Contracts.Contract;

namespace QuickFindAction.Plugins
{
    public class QuickFindPlugin : IPlugin
    {
        internal IOrganizationService OrganizationService
        {
            get;

            private set;
        }

        internal IPluginExecutionContext PluginExecutionContext
        {
            get;

            private set;
        }

        internal ITracingService TracingService
        {
            get;

            private set;
        }

        public void Execute(IServiceProvider serviceProvider)
        {
            Contract.Assert(serviceProvider != null, "serviceProvider is null");
            PluginExecutionContext =
                (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            TracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            Contract.Assert(TracingService != null, "TracingService is null");

            try
            {
                var factory =
                    (IOrganizationServiceFactory) serviceProvider.GetService(typeof (IOrganizationServiceFactory));

                OrganizationService = factory.CreateOrganizationService(this.PluginExecutionContext.UserId);

                Contract.Assert(PluginExecutionContext.InputParameters.Contains("SearchTextInput"), "No SearchTextInput property");
                Contract.Assert(
                    !string.IsNullOrEmpty(PluginExecutionContext.InputParameters["SearchTextInput"].ToString()), "SearchTextInput is null or empty");
                Contract.Assert(PluginExecutionContext.InputParameters.Contains("EntityNameInput"), "No EntityNameInput property");

                string searchText = PluginExecutionContext.InputParameters["SearchTextInput"].ToString(),
                       searchEntity = PluginExecutionContext.InputParameters["EntityNameInput"].ToString();

                var savedViewQuery = string.Format(
                    @"<fetch version=""1.0"" output-format=""xml-platform"" mapping=""logical"" distinct=""false"">
                      <entity name=""savedquery"">
                        <attribute name=""fetchxml"" />
                        <filter type=""and"">
                          <condition attribute=""statecode"" operator=""eq"" value=""0"" />
                          <condition attribute=""isquickfindquery"" operator=""eq"" value=""1"" />
                          <condition attribute=""isdefault"" operator=""eq"" value=""1"" />
                          <condition attribute=""name"" operator=""like"" value=""%{0}%"" />
                        </filter>
                      </entity>
                    </fetch>", searchEntity);

                var quickFindFetchXml =
                    OrganizationService.RetrieveMultiple(new FetchExpression(savedViewQuery)).Entities[0].GetAttributeValue<string>("fetchxml");
                    TracingService.Trace("FetchXml read from SavedView");
                    var entityFetchXml = XElement.Parse(string.Format(quickFindFetchXml, string.Format("%{0}%", searchText)));

                if (PluginExecutionContext.InputParameters["Page"] != null)
                {
                    entityFetchXml.SetAttributeValue("page", PluginExecutionContext.InputParameters["Page"]);
                }
                if (PluginExecutionContext.InputParameters["Count"] != null)
                {
                    entityFetchXml.SetAttributeValue("count", PluginExecutionContext.InputParameters["Count"]);
                }

                entityFetchXml.Elements().Elements("filter").Elements().ToList().ForEach(x => {
                                                                                                  if (
                                                                                                      x.Attribute(
                                                                                                          "attribute")
                                                                                                          .Value
                                                                                                          .EndsWith("id"))
                                                                                                  {
                                                                                                      x.SetAttributeValue("attribute",x.Attribute("attribute").Value+"name");
                                                                                                  } });
                PluginExecutionContext.OutputParameters["FetchXml"] = entityFetchXml.ToString();

                var results = OrganizationService.RetrieveMultiple(new FetchExpression(entityFetchXml.ToString()));
                PluginExecutionContext.OutputParameters["SearchResultsOutput"] = new EntityCollection(results.Entities.ToList());
            }
            catch (Exception e)
            {
                TracingService.Trace(e.StackTrace);
                PluginExecutionContext.OutputParameters["Exception"] = e.StackTrace;
                throw;
            }
        }
    }
}

Step 3: Register the plugin



Step 4: Create the Console Application
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Tooling.Connector;

namespace ActionsTester
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                var executeQuickFindRequest = new OrganizationRequest("ryr_Search");
                executeQuickFindRequest["SearchTextInput"] = "sus";
                executeQuickFindRequest["EntityNameInput"] = "contact";
                executeQuickFindRequest["Page"] = 1;
                //executeQuickFindRequest["Count"] = 1;

                var crmSvc =
                    new CrmServiceClient(new NetworkCredential("administrator", "p@ssw0rd1", "CRM"),
                                         AuthenticationType.AD, "crm1", "80", "Contoso");
                if (crmSvc.IsReady)
                {
                    crmSvc.OrganizationServiceProxy.Execute(executeQuickFindRequest);
                    OrganizationResponse response = crmSvc.OrganizationServiceProxy.Execute(executeQuickFindRequest);
                    if (response.Results.Contains("Exception") && response.Results["Exception"] != null)
                    {
                        Console.WriteLine(response.Results["Exception"]);
                        Console.WriteLine(response.Results["FetchXml"]);
                        return;
                    }
                    if (response.Results["SearchResultsOutput"] != null)
                    {
                        Console.WriteLine(response.Results["FetchXml"]);
                        var results = (EntityCollection) response.Results["SearchResultsOutput"];
                        foreach (var record in results.Entities)
                        {
                            record.Attributes.ToList().ForEach(x=> Console.WriteLine("{0}={1}",x.Key,Unwrap(x.Value)));
                            Console.WriteLine();
                        }
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.StackTrace);
            }
        }

        private static object Unwrap(object attributeValue)
        {
            var unwrappedValue = attributeValue;
            if (attributeValue is EntityReference)
            {
                unwrappedValue = ((EntityReference) attributeValue).Name;
            }
            else
                if (attributeValue is OptionSetValue)
                {
                    unwrappedValue = ((OptionSetValue)attributeValue).Value;
                }
            else
                if (attributeValue is Money)
                {
                    unwrappedValue = ((Money)attributeValue).Value;
                }
            return unwrappedValue;
        }
    }
}

Output:
The search term is "sus" and the entity is contact

Limitations:
1.) Search term is field type agnostic (exception of a lame entity reference mapping). So if a Optionset is in the quickfind query, the search term won't work properly
2.) Assumption is made that the quick find view name has the entity name. So if entity name is contact and the quick find view doesn't have the word contact this won't work properly.
3.) Assumption is made that schema name for lookups end with id.

Problems Faced:
I was initially using strongly typed entities with a generated service context in the plugin, but had some serialisation exceptions along the way and decided to switch to query approach to get the application working. The exception was

>System.Runtime.Serialization.SerializationException: Microsoft Dynamics CRM has experienced an error. Reference number for administrators or support: #59D91113: 
System.Runtime.Serialization.SerializationException: Element 'http://schemas.microsoft.com/xrm/2011/Contracts:Entity' contains data from a type that maps to the name 'Contoso.EarlyBound.Generated:Contact'. 
The deserializer has no knowledge of any type that maps to this name. Consider changing the implementation of the ResolveName method on your DataContractResolver to return a non-null value for name 'Contact' 
and namespace 'Contoso.EarlyBound.Generated'.
>   at System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(XmlReaderDelegator reader, String name, String ns, Type declaredType, DataContract& dataContract)
>   at System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(XmlReaderDelegator xmlReader, Int32 id, RuntimeTypeHandle declaredTypeHandle, String name, String ns)
>   at ReadArrayOfEntityFromXml(XmlReaderDelegator , XmlObjectSerializerReadContext , XmlDictionaryString , XmlDictionaryString , CollectionDataContract )
>   at System.Runtime.Serialization.CollectionDataContract.ReadXmlValue(XmlReaderDelegator xmlReader, XmlObjectSerializerReadContext context)
>   at System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(XmlReaderDelegator reader, String name, String ns, Type declaredType, DataContract& dataContract)
>   at System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(XmlReaderDelegator xmlReader, Int32 id, RuntimeTypeHandle declaredTypeHandle, String name, String ns)
>   at ReadEntityCollectionFromXml(XmlReaderDelegator , XmlObjectSerializerReadContext , XmlDictionaryString[] , XmlDictionaryString[] )
>   at System.Runtime.Serialization.ClassDataContract.ReadXmlValue(XmlReaderDelegator xmlReader, XmlObjectSerializerReadContext context)
>   at System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(XmlReaderDelegator reader, String name, String ns, Type declaredType, DataContract& dataContract)
>   at System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(XmlReaderDelegator xmlReader, Int32 id, RuntimeTypeHandle declaredTypeHandle, String name, String ns)
>   at ReadKeyValuePairOfstringanyTypeFromXml(XmlReaderDelegator , XmlObjectSerializerReadContext , XmlDictionaryString[] , XmlDictionaryString[] )
>   at System.Runtime.Serialization.ClassDataContract.ReadXmlValue(XmlReaderDelegator xmlReader, XmlObjectSerializerReadContext context)
>   at System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(XmlReaderDelegator reader, String name, String ns, Type declaredType, DataContract& dataContract)
>   at System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(XmlReaderDelegator xmlReader, Int32 id, RuntimeTypeHandle declaredTypeHandle, String name, String ns)
>   at ReadParameterCollectionFromXml(XmlReaderDelegator , XmlObjectSerializerReadContext , XmlDictionaryString , XmlDictionaryString , CollectionDataContract )
>   at System.Runtime.Serialization.CollectionDataContract.ReadXmlValue(XmlReaderDelegator xmlReader, XmlObjectSerializerReadContext context)
>   at System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(XmlReaderDelegator reader, String name, String ns, Type declaredType, DataContract& dataContract)
>   at System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(XmlReaderDelegator xmlReader, Type declaredType, DataContract dataContract, String name, String ns)
>   at System.Runtime.Serialization.DataContractSerializer.InternalReadObject(XmlReaderDelegator xmlReader, Boolean verifyObjectName, DataContractResolver dataContractResolver)
>   at System.Runtime.Serialization.XmlObjectSerializer.ReadObjectHandleExceptions(XmlReaderDelegator reader, Boolean verifyObjectName, DataContractResolver dataContractResolver)
>   at System.Runtime.Serialization.XmlObjectSerializer.ReadObject(XmlDictionaryReader reader)
>   at Microsoft.Crm.Sandbox.SandboxUtility.DeserializeDataContract[T](Byte[] serializedDataContract, Assembly proxyTypesAssembly)
>   at Microsoft.Crm.Sandbox.SandboxExecutionContext.Merge(IExecutionContext originalContext)
>   at Microsoft.Crm.Sandbox.SandboxCodeUnit.Execute(IExecutionContext context)


Observation:
1.) I was surprised to see address1_composite on the result set even though I did not mention it in the search find query. Running the exact same query in XrmToolBox FetchXml Tester doesn't return address1_composite field.
2.) count=0 on a fetchxml somehow works and returns records if matches are found.

Improvements that can be made:
1.) Parse the attributes in quickfind fetchxml and retrieve the types of these attributes, so that the search query can be correctly mapped.
2.) Use PFE Core Library on the retrieve part
3.) Add additional types to the unwrapping code in console application (currently unwraps only entityreference, optionsetvalue and money).

Conclusion:
Actions are AWESOME. I can write a logic once and can call this from console application, workflow or javascript. Previously you would have to encapsulate this logic on a webservice, to get this kind of extensibility.

Code: http://1drv.ms/1w5GJ2b

31 August 2014

Calculated fields using Plugin

Calculated fields will eventually make its way to Dynamics CRM. In the mean time, you can use ISV products like north52 Formula Manager, to achieve this functionality. If you are a developer and would like to implement a light weight version of calculated fields, this can be done using Plugins.

First we need a configuration entity to store the fetchxml that will be used for the calculation and the entity/attribute details that will use this on the Retrieve message.
In this case, the account field exp_invoicetotal will use the fetchxml result's totalworthofinvoices. This is an aliased field. Note that we use {0}, that will be replaced by the primary key of the entity that the plugin executes on.

Here is how the plugin is registered in the Plugin Registration tool. It executes on Retrieve of account.

Now the sourcecode for the actual plugin.

using CalculateOnRetrievePlugin.Entities;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;

namespace Experiments.Plugins
{
    public class CalculateOnRetrievePlugin : Plugin
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="CalculateOnRetrievePlugin"/> class.
        /// </summary>
        public CalculateOnRetrievePlugin()
            : base(typeof(CalculateOnRetrievePlugin))
        {
            //Third Param is primary entity name
            base.RegisteredEvents.Add(new Tuple<PipelinePhase, Message, string, Action<LocalPluginContext>>(PipelinePhase.PostOperation, Message.Retrieve, "account", ExecuteCalculateOnRetrievePlugin));

            // Note : you can register for more events here if this plugin is not specific to an individual entity and message combination.
            // You may also need to update your RegisterFile.crmregister plug-in registration file to reflect any change.
        }

        /// <summary>
        /// Executes the plug-in.
        /// </summary>
        /// <param name="localContext">The <see cref="LocalPluginContext"/> which contains the
        /// <see cref="IPluginExecutionContext"/>,
        /// <see cref="IOrganizationService"/>
        /// and <see cref="ITracingService"/>
        /// </param>
        /// <remarks>
        /// For improved performance, Microsoft Dynamics CRM caches plug-in instances.
        /// The plug-in's Execute method should be written to be stateless as the constructor
        /// is not called for every invocation of the plug-in. Also, multiple system threads
        /// could execute the plug-in at the same time. All per invocation state information
        /// is stored in the context. This means that you should not use global variables in plug-ins.
        /// </remarks>
        protected void ExecuteCalculateOnRetrievePlugin(LocalPluginContext localContext)
        {
            var targetEntity = (Entity)localContext.PluginExecutionContext.OutputParameters["BusinessEntity"];

            var queryConfiguration = new QueryExpression(ExpCalculatedfieldconfiguration.Fields.SchemaName)
                {
                    ColumnSet = new ColumnSet(
                        ExpCalculatedfieldconfiguration.Fields.ExpName,
                        ExpCalculatedfieldconfiguration.Fields.ExpResultField,
                        ExpCalculatedfieldconfiguration.Fields.ExpQuery,
                        ExpCalculatedfieldconfiguration.Fields.ExpFieldName,
                        ExpCalculatedfieldconfiguration.Fields.ExpIsAliasedField)
                };
            queryConfiguration.Criteria.AddCondition(new ConditionExpression(
                ExpCalculatedfieldconfiguration.Fields.ExpName, ConditionOperator.Equal,
                localContext.PluginExecutionContext.PrimaryEntityName));
            queryConfiguration.Criteria.AddCondition(new ConditionExpression(
                ExpCalculatedfieldconfiguration.Fields.Statecode, ConditionOperator.Equal, 0));
            var calcConfigRecords = localContext.OrganizationService.RetrieveMultiple(queryConfiguration).Entities;

            foreach (var calcConfigRecord in calcConfigRecords)
            {
                var isAliased = (bool)calcConfigRecord[ExpCalculatedfieldconfiguration.Fields.ExpIsAliasedField];
                var calculatedQueryResults = localContext.OrganizationService.RetrieveMultiple(
                    new FetchExpression(
                        string.Format(calcConfigRecord[
                        ExpCalculatedfieldconfiguration.Fields.ExpQuery].ToString(),
                        localContext.PluginExecutionContext.PrimaryEntityId)
                        )).Entities;

                foreach (var calculatedQueryResult in calculatedQueryResults)
                {
                    var targetFieldName =
                        calcConfigRecord[ExpCalculatedfieldconfiguration.Fields.ExpFieldName].ToString();
                    var resultFieldName =
                        calcConfigRecord[ExpCalculatedfieldconfiguration.Fields.ExpResultField].ToString();
                    var fieldValue = calculatedQueryResult[resultFieldName] as AliasedValue;
                    if (fieldValue != null)
                    {
                        targetEntity[targetFieldName] = ExtractString(fieldValue.Value);
                    }
                    else
                    {
                        targetEntity[targetFieldName] = 
                            ExtractString(calculatedQueryResult[resultFieldName]);
                    }
                }
            }
        }

        private string ExtractString(object attributeValue)
        {
            var attributeType = attributeValue.GetType(); 
            var result = string.Empty;
            if (attributeType == typeof (EntityReference))
            {
                result = ((EntityReference) attributeValue).Name;
            }
            else if (attributeType == typeof(OptionSetValue))
            {
                result = ((OptionSetValue)attributeValue).Value.ToString();
            }
            else if (attributeType == typeof(Money))
            {
                result = ((Money)attributeValue).Value.ToString("C");
            }
            else
            {
                return attributeValue.ToString();
            }
            return result;
        }
    }
}

Here is the Plugin base class.

using ExceptionMgmtServices;

namespace Experiments.Plugins
{
    using System;
    using System.Collections.ObjectModel;
    using System.Globalization;
    using System.Linq;
    using System.ServiceModel;
    using Microsoft.Xrm.Sdk;

    /// <summary>
    /// Base class for all Plugins.
    /// </summary>    
    public class Plugin : IPlugin
    {
        protected class LocalPluginContext
        {
            internal IServiceProvider ServiceProvider
            {
                get;

                private set;
            }

            internal IOrganizationService OrganizationService
            {
                get;

                private set;
            }

            internal IPluginExecutionContext PluginExecutionContext
            {
                get;

                private set;
            }

            internal ITracingService TracingService
            {
                get;

                private set;
            }

            private LocalPluginContext()
            {
            }

            internal LocalPluginContext(IServiceProvider serviceProvider)
            {
                if (serviceProvider == null)
                {
                    throw new ArgumentNullException("serviceProvider");
                }

                // Obtain the execution context service from the service provider.
                this.PluginExecutionContext = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

                // Obtain the tracing service from the service provider.
                this.TracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));

                // Obtain the Organization Service factory service from the service provider
                IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));

                // Use the factory to generate the Organization Service.
                this.OrganizationService = factory.CreateOrganizationService(this.PluginExecutionContext.UserId);
            }

            internal void Trace(string message)
            {
                if (string.IsNullOrWhiteSpace(message) || this.TracingService == null)
                {
                    return;
                }

                if (this.PluginExecutionContext == null)
                {
                    this.TracingService.Trace(message);
                }
                else
                {
                    this.TracingService.Trace(
                        "{0}, Correlation Id: {1}, Initiating User: {2}",
                        message,
                        this.PluginExecutionContext.CorrelationId,
                        this.PluginExecutionContext.InitiatingUserId);
                }
            }
        }

        private Collection<Tuple<PipelinePhase, Message, string, Action<LocalPluginContext>>> registeredEvents;

        /// <summary>
        /// Gets the List of events that the plug-in should fire for. Each List
        /// Item is a <see cref="System.Tuple"/> containing the Pipeline Stage, Message and (optionally) the Primary Entity. 
        /// In addition, the fourth parameter provide the delegate to invoke on a matching registration.
        /// </summary>
        protected Collection<Tuple<PipelinePhase, Message, string, Action<LocalPluginContext>>> RegisteredEvents
        {
            get
            {
                if (this.registeredEvents == null)
                {
                    this.registeredEvents = new Collection<Tuple<PipelinePhase, Message, string, Action<LocalPluginContext>>>();
                }

                return this.registeredEvents;
            }
        }

        /// <summary>
        /// Gets or sets the name of the child class.
        /// </summary>
        /// <value>The name of the child class.</value>
        protected string ChildClassName
        {
            get;

            private set;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Plugin"/> class.
        /// </summary>
        /// <param name="childClassName">The <see cref=" cred="Type"/> of the derived class.</param>
        internal Plugin(Type childClassName)
        {
            this.ChildClassName = childClassName.ToString();
        }

        /// <summary>
        /// Executes the plug-in.
        /// </summary>
        /// <param name="serviceProvider">The service provider.</param>
        /// <remarks>
        /// For improved performance, Microsoft Dynamics CRM caches plug-in instances. 
        /// The plug-in's Execute method should be written to be stateless as the constructor 
        /// is not called for every invocation of the plug-in. Also, multiple system threads 
        /// could execute the plug-in at the same time. All per invocation state information 
        /// is stored in the context. This means that you should not use global variables in plug-ins.
        /// </remarks>
        public void Execute(IServiceProvider serviceProvider)
        {
            if (serviceProvider == null)
            {
                throw new ArgumentNullException("serviceProvider");
            }

            // Construct the Local plug-in context.
            LocalPluginContext localcontext = new LocalPluginContext(serviceProvider);

            localcontext.Trace(string.Format(CultureInfo.InvariantCulture, "Entered {0}.Execute()", this.ChildClassName));

            try
            {
                // Iterate over all of the expected registered events to ensure that the plugin
                // has been invoked by an expected event
                // For any given plug-in event at an instance in time, we would expect at most 1 result to match.
                Action<LocalPluginContext> entityAction =
                    (from a in this.RegisteredEvents
                     where (
                     (int)a.Item1 == localcontext.PluginExecutionContext.Stage &&
                     a.Item2.ToString() == localcontext.PluginExecutionContext.MessageName &&
                     (string.IsNullOrWhiteSpace(a.Item3) || a.Item3 == localcontext.PluginExecutionContext.PrimaryEntityName)
                     )
                     select a.Item4).FirstOrDefault();

                if (entityAction != null)
                {
                    localcontext.Trace(string.Format(
                        CultureInfo.InvariantCulture,
                        "{0} is firing for Entity: {1}, Message: {2}",
                        this.ChildClassName,
                        localcontext.PluginExecutionContext.PrimaryEntityName,
                        localcontext.PluginExecutionContext.MessageName));

                    entityAction.Invoke(localcontext);

                    // now exit - if the derived plug-in has incorrectly registered overlapping event registrations,
                    // guard against multiple executions.
                    return;
                }
            }
            catch (Exception e)
            {
                localcontext.Trace(string.Format(CultureInfo.InvariantCulture, "Exception: {0}", e.ToString()));
                //string friendlyMessage = ExceptionManagement.LogException(e, PriorityLevel.HIGH, localcontext.OrganizationService);
                // Handle the exception.
                //throw new Exception(friendlyMessage);
                throw;
            }
            finally
            {
                localcontext.Trace(string.Format(CultureInfo.InvariantCulture, "Exiting {0}.Execute()", this.ChildClassName));
            }
        }

        public enum PipelinePhase
        {
            PreValidation = 10,
            PreOperation = 20,
            MainOperation = 30,
            PostOperation = 40
        }

        public enum Message
        {
            AddListMembers,
            AddMember,
            AddMembers,
            AddPrivileges,
            AddProductToKit,
            AddRecurrence,
            AddToQueue,
            AddUserToRecordTeam,
            Assign,
            AssignUserRoles,
            Associate,
            BackgroundSend,
            Book,
            Cancel,
            CheckIncoming,
            CheckPromote,
            Clone,
            Close,
            CopyDynamicListToStatic,
            CopySystemForm,
            Create,
            CreateException,
            CreateInstance,
            Delete,
            DeleteOpenInstances,
            DeliverIncoming,
            DeliverPromote,
            DetachFromQueue,
            Disassociate,
            Execute,
            ExecuteById,
            Export,
            ExportAll,
            ExportCompressed,
            ExportCompressedAll,
            GrantAccess,
            Handle,
            Import,
            ImportAll,
            ImportCompressedAll,
            ImportCompressedWithProgress,
            ImportWithProgress,
            LockInvoicePricing,
            LockSalesOrderPricing,
            Lose,
            Merge,
            ModifyAccess,
            Publish,
            PublishAll,
            QualifyLead,
            Recalculate,
            RemoveItem,
            RemoveMember,
            RemoveMembers,
            RemovePrivilege,
            RemoveProductFromKit,
            RemoveRelated,
            RemoveUserFromRecordTeam,
            RemoveUserRoles,
            ReplacePrivileges,
            Reschedule,
            Retrieve,
            RetrieveExchangeRate,
            RetrieveFilteredForms,
            RetrieveMultiple,
            RetrievePersonalWall,
            RetrievePrincipalAccess,
            RetrieveRecordWall,
            RetrieveSharedPrincipalsAndAccess,
            RetrieveUnpublished,
            RetrieveUnpublishedMultiple,
            RevokeAccess,
            Route,
            Send,
            SendFromTemplate,
            SetRelated,
            SetState,
            SetStateDynamicEntity,
            TriggerServiceEndpointCheck,
            UnlockInvoicePricing,
            UnlockSalesOrderPricing,
            Update,
            ValidateRecurrenceRule,
            Win
        }
    }
}

I have used Gayan Perera's CRM Code Generator to generate the entity classes using the CSharp.tt template, which I have used in the plugin. This plugin inserts the result of the fetchxml in the calculated field configuration entity, as an attribute in the BusinessEntity, which is accessible from the OutputParameter in the plugin. BusinessEntity is one of the output parameters in the Retrieve response.

Here is the how the entity form looks with the calculated field, which I have made as readonly.
Number of cases, Number of Contacts and Invoice Total have all been calculated by the plugin using the configuration entity and hence can be viewed when the user opens the account form. Since this plugin is registered only on the Retrieve message this will not work when you do a RetrieveMultiple or execute a Saved Query or User Query. If additional calculated fields are required on a different entity, a new plugin step has to be registered for the retrieve message of that entity. A new calculated field configuration record also has to be created for this entity.

The things I like about this approach is that it is very easily to manipulate the fetchxml and display the result in a field and I find it very useful for aggregating the child records to the parent. I have done this a quick proof of concept, to visualise the idea.