Plugin Integration Tests using FakeXrmEasy

XrmUnitTest and FakeXrmEasy are two testing frameworks that are specifically targeted towards Dynamics CRM/Dynamics 365. It is possible to do both unit test as well as integration tests using both these frameworks. In this first post, I would like to cover FakeXrmEasy. Here is a sample plugin code that I would like to test:

using Microsoft.Xrm.Sdk;
using System;
using Microsoft.Crm.Sdk.Messages;

namespace SamplePlugin
{
    public class LastNameUpperCasePlugin : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            ITracingService tracer = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = factory.CreateOrganizationService(context.UserId);

            try
            {
                Entity entity = (Entity)context.InputParameters["Target"];
                //not required. Just added to demonstrate that we are connecting to a real crm org service
                var response = service.Execute(new WhoAmIRequest());

                var lastName = entity.GetAttributeValue<string>("lastname");
                if (!string.IsNullOrEmpty(lastName))
                {
                    entity["lastname"] = lastName.ToUpper();
                }
            }
            catch (Exception e)
            {
                throw new InvalidPluginExecutionException(e.Message);
            }
        }
    }
}

In order to test this plugin, we have to create a unit test project. Here are the steps for it:

  1. Create a unit test  project from the existing plugin solution by right clicking and choose Add->New Project->Visual C#->Test->Unit Test Project
  2. Use nuget to add the correct FakeXrmEasy reference to the test project. In this example I am using “FakeXrmEasy.365” as I am connecting to a Dynamics 365 instance online. Refer https://www.nuget.org/profiles/jmontana to find out all the different versions that are relevant to your CRM version.
  3. Create a App.config file, if it doesn’t already exist. Here is my connection string. Refer https://msdn.microsoft.com/en-us/library/mt608573.aspx to find the correct format of the connection string for you crm instance.connection-string

Below is my test for the plugin

using System;
using System.Collections.Generic;
using System.Web.Configuration;
using FakeXrmEasy;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Xrm.Sdk;

namespace SamplePlugin.Test
{
	[TestClass]
	public class LastNamePluginUnitTests
	{
		[TestMethod]
		public void Should_Set_LastName_In_UpperCase()
		{
			var context = new XrmRealContext
			{
				ProxyTypesAssembly = typeof(LastNameUpperCasePlugin).Assembly,
				ConnectionStringName = "CRMOnline"
			};
			var executionContext = context.GetDefaultPluginContext();
			var target = new Entity("contact")
			{
				["lastname"] = "Power",
				Id = Guid.NewGuid()
			};
			executionContext.MessageName = "Create";
			executionContext.Stage = 20;
			executionContext.PrimaryEntityId = target.Id;
			executionContext.PrimaryEntityName = target.LogicalName;
			executionContext.InputParameters = new ParameterCollection
			{
				new KeyValuePair<string, object>("Target", target)
			};
			context.ExecutePluginWith<LastNameUpperCasePlugin>(executionContext);
			Assert.AreEqual("POWER",
				((Entity) executionContext.InputParameters["Target"]).GetAttributeValue<string>("lastname"));
		}
	}
}

In this test I am creating a scenario where the plugin is running pre-create. You can change to add pre/post entity images or target a different message and stage. If we debug our integration test, we can see that we are connected to the real org service.

Debugger.png

I have used latebound entity in this example, but you can use the same code with early bound entities generated using crmsvcutil or Early Bound Generator.

You can use a similar approach for testing custom workflow assemblies as well. In that case you would be using “GetDefaultWorkflowContext” instead of “GetDefaultPluginContext” to get the execution context.

Footnote: Technically this test could have been better written as an unit test instead of a integration test. But the purpose of this post is to demonstrate the points below:

  1. How to build a fake plugin execution context
  2. How to use connection strings to create a real OrganizationService instance
  3. How to use the real OrganizationService with the fake plugin execution context

EDIT (22/11/2016): Thank you Jonas (@rappen) for pointing out the plugin stage should be pre-create for this to work. Updated context stage to 20.

Searching by Guid

When a lookup is resolved in CRM in advanced find on entity form, it uses quick find view to resolve the correct record. So, if you search for a value that is present in an attribute that is not a “Find Column” the value will not be resolved to a lookup. In this situation, you will get a red cross icon, indicating that this value has to be manually chosen.

Failed to resolve

Note in the above screenshot that I am searching by Guid of the contact. The primary key of the contact entity is “contactid”, but it cannot be added as a “Quick Find” column.

QuickFind Columns

The default find column of a custom entity will be “[publisher prefix]_name”. This is usually the “Primary field” of the custom entity, unless you have changed it during entity creation. If you just went with the default, this is what it will be. Unlike OOB system entities, which have lot of find columns defined by default, custom entity will have one find column i.e. primary field, defined by default.

In both these scenarios it is not possible to search by the primary key of the record, as indicated by the screenshot above. As a power user, you might know the primary key of the record you exactly want on the lookup. A common scenario that I personally experienced is that, I might create a fetchxml that returns a child result set, and I want to display the parent entity records from advanced find, that have the child lookup set to a particular value.

You could search by name if name is unique or your quickfind columns don’t return multiple results. If you search term returns multiple matches, you will get an yellow exclamation icon indicating that you would have manually resolve it.

Multiple Matches

In this situation where you are searching a contact, it is not uncommon for people to have the exact same name, and if you are searching by name (assuming fullname is a find column) you will end up in this scenario. I present a solution to this problem. It is a custom plugin that fires pre-operation of RetrieveMultiple message and modifies the query to enable searching by primary key.

This is how the plugin is registered.

Plugin

Registration

In this case, I want the primary key search capability only for account and contact, and hence registered steps only for these entities. You could add additional steps similarly for more entities, that you require.

Here is the functionality in action on lookup.

Advanced Find Guid Search

Here is the same behavior on normal saved view search.

View Guid Search

You can find the source code for the plugin at https://github.com/rajyraman/QuickFindManipulate

This a proof-of-concept to give a idea about this capability using RetrieveMultiple. You can extend on this to make the plugin steps configurable and add additional performance optimisations. I hope this will be a useful feature for power users.

Quicktip: Debugging merged plugin assembly

Plugin profiling and debugging is a very useful feature in the plugin registration tool. It helps you to debug and step through the plugin code in your local machine with Visual Studio, using the serialised plugin context. The serialised context can also be stored in CRM and debug can be initiated from this record.

Profile Screen

There is one small issue when you try to debug a IL Merged assembly. When you try to profile such a plugin assembly, you will get an error like this.

PluginRegTool Error

The error is saying that the plugin profiler cannot locate the assembly that was merged with the main plugin assembly.

Resolution:

Copy all the assemblies that were merged with the plugin assembly that you are trying to debug, into the same directory as the Plugin Registration tool executable.

I experienced this issue when I was trying to debug a plugin assembly merged using the CRM Solution Manager.

Further reading:

Debug a plugin – https://msdn.microsoft.com/en-us/library/gg328574.aspx

Gotcha: Plugin Running Async

Generally when you want run a set of tasks that are not realtime, you would do this via Workflow. On the contrary when you want something to happen straightaway, you would do this using a Plugin. Before CRM2015, there was only one type of workflow. Starting with CRM2015 a workflow can be realtime or async. A plugin also can be run asynchronously or synchronously. To summarise quickly:

  1. A realtime workflow is similar to a plugin
  2. An asynchronous plugin is similar to a workflow

Take this particular scenario: You have a plugin that runs synchronously on the Update of a certain entity. Later down the track, you decide you want this plugin to run async, as the set of tasks performed by the plugin doesn’t really have to be realtime and you want to improve the performance of the core operation.

In this scenario, you have to be mindful of one particular plugin behaviour: transaction. When an exception is thrown inside a plugin, the core operation won’t succeed as the exception in the plugin will cause the transaction to rollback. When you change the Execution Mode to “Asynchronous”, you will still see that the plugin has failed in System Jobs area, but the transaction won’t be rolled back.

For eg. take this simple code:

using Microsoft.Xrm.Sdk;
using System;

namespace CrmExperimentPlugin
{
    public class CreateContactPlugin : IPlugin
    {
        #region Secure/Unsecure Configuration Setup
        private string _secureConfig = null;
        private string _unsecureConfig = null;

        public CreateContactPlugin(string unsecureConfig, string secureConfig)
        {
            _secureConfig = secureConfig;
            _unsecureConfig = unsecureConfig;
        }
        #endregion
        public void Execute(IServiceProvider serviceProvider)
        {
            ITracingService tracer = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = factory.CreateOrganizationService(context.UserId);
            Entity entity = (Entity)context.InputParameters["Target"];

            Entity contact = new Entity("contact");
            contact["firstname"] = "Max";
            contact["lastname"] = "Power";
            service.Create(contact);

            throw new InvalidPluginExecutionException("Exception raised after contact create");
        }
    }
}

Assuming that the plugin is registered post-create on a particular entity and Execution Mode is “Synchronous”, there will be no contact called “Max Power” at the end of the plugin execution. But if the same plugin is registered as post-create and asynchronous, there will be a contact called “Max Power” as the exception doesn’t rollback the transaction.

If you reference Event Execution Pipeline in msdn, it says this about the plugin transaction:

Stages 20 and 40 are guaranteed to be part of the database transaction while stage 10 may be part of the transaction.

But this is not entirely true, as it also depends on the Execution Mode.

tl;dr; Register plugins to run sychronously, if you need the transaction to rollback on plugin exception and need to validate certain criteria for the core operation to succeed. If you don’t need these features, use a workflow.

Performing Outer Join in Advanced Find

It is currently not possible to perform left outer join using Advanced Find, even though that capability exists in CRM2015. For e.g. if you want to get a list of contacts, that don’t have an invoice, you cannot do it using an ad-hoc Advanced Find.

This limitation is usually overcome by creating a system view that does a regular inner join, and then updating the view’s fetchxml using a tool like FXB or even editing the customisation file directly. There is also the rollup field approach described in http://crmtipoftheday.com/2015/04/27/not-in-for-the-rest-of-us/. But if you are going to do an ad-hoc advanced find query, you really don’t want to create one system view for each entity, for which you want to do an outer join.

I present to you a solution for this problem: AdvancedQueryOuterJoin

This is a plugin that runs on the pre-stage of RetrieveMultiple, and alters the fetchxml with the correct left outer join condition. This means you can do an Advanced Find Query like this one below.

OuterJoin

The above query is for Account, who have child contacts, but don’t have any opportunities. When the plugin sees a link entity in the query, with a null primary key condition, it replaces that condition with a left outer join on the parent entity. The code itself is very simple, and you can have a look at the github repo to understand how this works.

You can download the solution from https://github.com/rajyraman/AdvancedQueryOuterJoin/releases. Download the solution file that works best for your CRM instance i.e. 7.0 or 7.1. Please also use the github issues area, for entering details about any problems that you encounter.

EDIT (01/06/15): Using this plugin will not correct the fetchxml you download by clicking the Download Fetch XML in the Advanced Find window. If you are going to run your downloaded query in FXB, make sure you change the Output type to Grid.

FXB

If you use FetchResult, you won’t see any results, as the plugin would not have executed in the background.

EDIT (02/10/15): Refer to the newer post https://dreamingincrm.com/2015/09/14/bookmarklet-advanced-find-outer-join-a-k-a-not-in-condition/ for doing this using bookmarklet.

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.

Executing Quick Find from Console Application : Redux

If you have been following my blog, you might remember this (https://dreamingincrm.com/2014/05/07/executing-quickfind-using-crm-sdk/) 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") &amp;&amp; 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&amp; 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&amp; 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&amp; 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&amp; 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&amp; 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)

Observations:
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