Posted on 6. June 2014

Monitor, Monitor, Monitor

I once heard someone say that "the great thing about Dynamics CRM is that it just looks after itself" Whilst CRM2013 is certainly very good at performing maintenance tasks automatically, if you have a customised system it is important to Monitor, Monitor, Monitor! There are some advanced ways of setting up monitoring using tools such as System Center but just some regular simple monitoring tasks will go a long way for very little investment on your part:

1) Plugin Execution Monitoring

There is a super little entity called 'Plugin-in Type Statistics' that often seems to be overlooked in the long list of advanced find entities. This entity is invaluable for tracing down issues before they cause problems for your users and as defined by the SDK it is "used by the Microsoft Dynamics CRM 2011 and Microsoft Dynamics CRM Online platforms to record execution statistics for plug-ins registered in the sandbox (isolation mode)."

The key here is that it only records statistics for your sandboxed plugins. Unless there is a good reason not to (security access etc.) I would recommend that all of your plugins be registered in sandbox isolation. Of course Dynamics CRM online only allows sandboxed plugins anyway so you don't want to put up barriers not to move to the cloud.

To monitor this you can use advanced to show a sorted list by execution time or failure count descending:

If you spot any issues you can then proactively investigate them before they become a problem. In the screen shot above there are a few plugins that are taking more than 1000ms (1 second) to execute, but their execution count is low. I look for plugins that have a high execution count and high execution time, or those that have a high failure percent.

2) Workflow & Asynchronous Job Execution Monitoring

We all know workflows often can start failing for various reasons. Because of their asynchronous nature these failures can go unnoticed by users until it's too late and you have thousands of issues to correct. To proactively monitor this you can create a view (and even add to a dashboard) of System Jobs filtered by Status = Failed or Waiting and where the Message contains data. The Message attribute contains the full error description and stack trace, but the Friendly message just contains the information that is displayed at the top of the workflow form in the notification box.

3) Client Latency & Bandwidth Monitoring

Now that you've got the server-side under control you should also look at the client connectivity of your users. There is a special diagnostics hidden page that can be accessed by using a URL of the format:

http://<YourCRMServerURL>/tools/diagnostics/diag.aspx

As described by the implementation guide topic, "Microsoft Dynamics CRM is designed to work best over networks that have the following elements:

  • Bandwidth greater than 50 KB/sec
  • Latency under 150 ms"

After you click 'Run' on this test page you will get results similar to that shown below. You can see that this user is just above these requirements!

You can read more about the Diagnostic Page in Dynamics CRM. You can also monitor the client side using the techniques I describe in my series on Fiddler:

If you take these simple steps to proactively monitor your Dynamics CRM solution then you are much less likely to have a problem that goes un-noticed until you get 'that call'!

@ScottDurow

Posted on 28. February 2014

Real Time Workflow or Plugin?

I have been asked "should I use Real Time Workflows instead of Plugins?" many times since CRM2013 first introduced this valuable new feature. Real Time Workflows (RTWFs) certainly have many attractive benefits over Plugins including:

  • Uses the same interface as standard workflows making it very quick & simple to perform straightforward tasks.
  • Can be written & extended without coding skills
  • Easily extend using custom workflow activities that can be re-used in many places.

If we can use custom workflow activities to extend the native workflow designer functionality (e.g. making updates to records over 1:N and N:N relationships) then it raises the question why should we use Plugins at all?

I am a big fan of RTWFs – they add considerable power to the framework that ultimately makes the solution more effective and lowers the implementation costs. That said I still believe that considering plugins is still an important part of any Dynamics CRM Solution design - the reasons can be split into the following areas:

Performance

Performance is one of those areas that it is very tempting to get bogged down by too early by 'premature optimisation'. In most cases performance should be considered initially by adhering to best practices (e.g. not querying or updating more data than needed) and ensuring that you use supported & documented techniques. If we ensure that our system is structured in an easy to follow and logical way then Dynamics CRM will usually look after performance for us and enable us to scale up and out when it is needed (Dynamics CRM Online will even handle this scaling for you!). It is true there are some 'gotchas' that are the exception to this rule, but on the whole I think primarily it is better to design for maintainability and simplicity over performance in the first instance. Once you have a working design you can then identify the areas that are going to be the main bottle necks and then focus optimisation efforts on those areas alone. If we try to optimise all parts of the system from the start when there is no need I find that it can actually end up reducing overall performance and end up with an overly complex system that has a higher total cost of ownership.

It is true that Plugins will out performance RTWFs in terms of through-put but if the frequency of the transactions are going to be low then this usually will not be an issue. If you have logic that is going to be firing at a high frequency by many concurrent users then this is the time consider selecting a plugin over a RTWF.

In some simple tests I found that RTWFs took twice (x2) as long as a Plugin when performing a simple update. The main reason for this is that the plugin pipeline is a very efficient mechanism for inserting logic inside of the platform transaction. A RTWF inserts an additional transaction inside that parent transaction that contains many more database queries required to set up the workflow context. The component based nature of workflow activity steps means the same data must be read multiple times to make each step work in isolation from the others. Additionally, if you update a record in a RTWF, it will apply an update immediately to the database within this inner transaction. This database update is in addition to the overall plugin update. Using a plugin there will only be a single update since the plugin is updating the 'in transit' pipeline target rather than the database.

Another reason that the RTWF takes longer to complete the transaction is that it appears to always retrieve the entire record from the database even when using only a single value.

Pre/Post Images

When using plugins you have a very fine control over determining what data is being updated and can see what the record looked like before the transaction and what it would look like after the transaction. RTWFs don't offer you this same control and so if you need to determine if a value has changed from a specific value to another specific value (say a specific state transition) it is harder to determine what the value was before the workflow started. When a RTWF reads a record from the database, it will load all values, but with a plugin you can select only a small number of attributes to include in the pipeline or query.

Impersonation

RTWFs allow you to select whether to run as the calling user or the owner of the workflow, where a Plugin gives you full control to execute an SDK call as the system user or an impersonated user at different points during the transaction.

Code vs Configuration

With RTWFs your business logic tends to become fragmented over a much higher surface area of multiple child RTWFs. This makes unit testing and configuration control much harder. With a plugin you can write unit tests and check the code into TFS. With every change you can quickly see the differences from the previous version and easily see the order that the code is executed.

I find that it is good practice to have a single RTWF per triggering event/entity combination and call child workflows rather than have many RTWFs kicked off by the same trigger, otherwise there is no way of determining the order that they will execute in.

A very compelling reason to use plugins is the deterministic nature of their execution. You can control the order that your code executes in by simply sequencing the logic in a single plugin and then unit test this sequence of events by using a mock pipeline completely outside of the Dynamics CRM Runtime.

So just tell me which is best?!

Which is best? Each have their strengths and weaknesses so the answer is "it depends" (as it so often is!).

After all this is said and done – a very compelling reason to use RTWS is the fact that business users can author and maintain them in a fraction of the time it takes to code and deploy a plugin and often will result in far fewer bugs due to the component based building blocks.

As a rule of thumb, I use two guiding principles:

  1. If there is already custom plugin code on an entity (or it is planned), then use a plugin over a RTWF to keep the landscape smaller and deterministic.
  2. If you know up-front that you need a very high throughput for a particular function with a high degree of concurrency, then consider a plugin over a RTWF for this specific area.

@ScottDurow

Posted on 5. January 2013

Multi-Language Lookups

Happy New Year!

Dynamics CRM has fantastic localisation support and multiple language user interfaces is no exception to that rule. Installable language packs provide translations for out-of-the box labels, whilst customizers are able to translate their own labels for the following elements:

  • Field Labels
  • Option Set Values
  • Entity Display Names
  • User Interface Messages
  • View Names
  • Ribbon Button Labels
  • Descriptions

More information about this can be found at Customize Labels to Support Multiple Languages, but the purpose of this post is to provide a suggested solution to localising the only item that is missing from this list: Lookup Record Display Names

If you had two sales teams that spoke only English or German and they wanted to share the same products, there currently is no way of showing the product name field in the sales persons own language without duplicating product records.

My suggested solution is to write a plugin to intercept the results returned from the database when queried and insert the correct language into the name field.

The example I show below works for Products and Opportunity Products, but it could equally be applied to other entities as well.

Key design objectives were:

  • Optimise for performance by minimising database queries and caching where the same data where possible.
  • Allow translation of the Product Name attribute of the Product Entity.
  • Show the Product Name on the Opportunity Product form in the user's language.
  • Show the Product Name in Lookup Views in the user's language
  • Show the Product Name title on the Product form in the user's language.
  • Support translated Product Name fields in reports
  • Allow easy translation of products into multiple languages an export/import process.

Solution Summary

The technique adds an attribute for each name translation on the product form, and then intercepts Retrieve and Retrieve-Multiple pipeline steps in order to substitute the name field for the correct translation.

1) An attribute for each name translation is added to the Product entity

2) When the product is updated, each translated is packed into the 'name' field via the 'Create' and 'Update' pipeline steps. The name attribute is increased in length to hold 4000 characters to accommodate all the translated names. This is done so that all translations are available when querying the name attribute without going back to the database.

new_name_en: "Red Shoes"
new_name_de: "Rote Schuhe"
name: "Red Shoes,Rote Schuhe"

Each time the name attribute is retrieved, we can always re-write it to include the language that we need without going back to the database to get the new_name_en or new_name_de field values.

Note: A limitation of the primary name field is that it has a maximum length of 4,000 characters. The product name is included in a SQL index and so cannot be more than 450 characters (900 bytes). Due to the nature of this technique, we must be able to fit all our translations into these characters, so if we have 5 languages, then each name cannot be more than 90 characters long – since the default Name length is 100 characters, this doesn't seem that unreasonable – especially if you have less than 5 languages. If you are using this technique on a custom entity attribute, then you can use the full 4000 characters.

3) When the Products are queried (via Lookup dialogs or Advanced Find) the results are changed so that the name field only contains the translation that matches the language of the user.

4) When the Opportunity Product is Retrieved, the name field is re-written to only contains the translation that matches the language of the user.

5) The user's selected language is looked up in the UserSetting entity via the UILanguageId attribute. This contains an LCID that is used to match against the correct translated label. 

Solution Steps

First we need to create a project to hold the plugin:

1) Create a Plugin Solution using the CRM Developer Toolkit found in the CRM2011 SDK.

2) Connect to your CRM server and select a solution to add your plugin to.

3) Select the Plugin and Workflow project in turn and open the properties window. Select 'Signing' and check 'Sign the assembly' before selecting '<New…>'. You will be prompted to give your key a name and password. I usually use the name of the project as a name for the key.

4) Open the CRM Explorer window and double click 'Product' to open the Product entity configuration page.

5) Select the 'name' attribute, and change its 'Maximum Length' to 450 characters, and the required level to 'No Constraint'

6) We need to create some attributes to hold the translated names of the products. I am only creating English and German, but you can create however many you need.

Display Name 

Schema Name 

Type 

Name (English) 

new_name_en 

Single Line of Text (100) 

Name (German) 

new_name_de 

Single Line of Text (100) 


7) Add the two fields to the Product Form as well, and un-check 'Visible by default' on the 'Name' field.

8) Publish the customisations.

9) In your Visual Studio Project, add a new Class named 'MultiLanguageProductPlugin'

10) Paste the following code into your class.
IMPORTANT: Change the namespace to match your project namespace

// Scott Durow
// 1/2/2013 7:55:23 PM
// Multi Language Support for Lookups
namespace Develop1.Plugins
{
    using System;
    using System.ServiceModel;
    using Microsoft.Xrm.Sdk;
    using System.Text;
    using Microsoft.Xrm.Sdk.Query;


    /// 
    /// Plugin to support Multi Language Product Names
    ///     
    public class MultiLanguageProductPlugin: Plugin
    {
        private readonly string preImageAlias = "PreImage";
        private readonly string[] languages = new string[] { "en", "de" }; // Languages Supported
        private readonly int[] locales = new int[] { 1033, 1031 }; // LCIDs of each language in the languages array

        /// 
        /// Initializes a new instance of the  class.
        /// 
        public MultiLanguageProductPlugin()
            : base(typeof(MultiLanguageProductPlugin))
        {
            // Registrations for Packing each translation field into the name field
            base.RegisteredEvents.Add(new Tuple>(20, "Create", "product", 
                new Action(PackNameTranslations)));
            base.RegisteredEvents.Add(new Tuple>(20, "Update", "product", 
                new Action(PackNameTranslations)));

            // Registrations for unpacking the name field on Retrieve of Products
            base.RegisteredEvents.Add(new Tuple>(40, "Retrieve", "product", 
                new Action(UnpackNameOnRetrieve)));
            base.RegisteredEvents.Add(new Tuple>(40, "RetrieveMultiple", "product", 
                new Action(UnpackNameOnRetrieveMultiple)));

            // Registrations for unpacking the name field on related Opportunity Products
            // NOTE: You could add registratons for Quotes, Orders, Price Lists here...
            base.RegisteredEvents.Add(new Tuple>(40, "Retrieve", "opportunityproduct", 
                new Action(UnpackNameOnRetrieveRelated)));
            base.RegisteredEvents.Add(new Tuple>(40, "RetrieveMultiple", "opportunityproduct", 
                new Action(UnpackNameOnRetrieveMultipleRelated)));
           
        }

        /// 
        /// Pack the translations into the name field when a Product is Created or Updated
        /// Each translated name is packed into a comma separated string
        /// This field is unpacked when the product entity is retrieved or related records are retrieved
        /// 
        protected void PackNameTranslations(LocalPluginContext localContext)
        {
           
            IPluginExecutionContext context = localContext.PluginExecutionContext;

            // Pack the translated labels into the name field en,de
            Entity target = (Entity)localContext.PluginExecutionContext.InputParameters["Target"];
            Entity preImageEntity = (context.PreEntityImages != null && context.PreEntityImages.Contains(this.preImageAlias)) ? context.PreEntityImages[this.preImageAlias] : null;

            string[] names = new string[languages.Length];

            for (int i = 0; i < languages.Length; i++)
            {
                names[i] = GetAttributeValue("new_name_" + languages[i], preImageEntity, target);

            }

            // Store the packed value in the target entity
            target["name"] = string.Join(",", names);
        }

        /// 
        ///  Unpack the name field when a Product is Retreived
        /// 
        protected void UnpackNameOnRetrieve(LocalPluginContext localContext)
        {    
            IPluginExecutionContext context = localContext.PluginExecutionContext;
            Entity target = (Entity)context.OutputParameters["BusinessEntity"];

            // Re-write the name field in the retrieved entity
            target["name"] = UnpackName(localContext, target.GetAttributeValue("name"));   
        }

        /// 
        /// Unpack the name field when Products are retrieved via Lookup Search or Advanced Find
        /// 
        protected void UnpackNameOnRetrieveMultiple(LocalPluginContext localContext)
        {
           IPluginExecutionContext context = localContext.PluginExecutionContext;
           EntityCollection collection = (EntityCollection) localContext.PluginExecutionContext.OutputParameters["BusinessEntityCollection"];
           foreach (Entity e in collection.Entities)
           {
               if (e.Attributes.ContainsKey("name"))
               {
                   e["name"] = UnpackName(localContext, e.GetAttributeValue("name"));
               }
           }
        }
        /// 
        /// Unpack the product lookup name when an Opportunity Producs is Retrieved
        /// 
        protected void UnpackNameOnRetrieveMultipleRelated(LocalPluginContext localContext)
        {
            IPluginExecutionContext context = localContext.PluginExecutionContext;
            EntityCollection collection = (EntityCollection)localContext.PluginExecutionContext.OutputParameters["BusinessEntityCollection"];
            foreach (Entity e in collection.Entities)
            {
                if (e.Attributes.ContainsKey("productid"))
                {
                    ((EntityReference)e["productid"]).Name = UnpackName(localContext, e.GetAttributeValue("productid").Name);
                }
            }
        }

        /// 
        /// Unpack the product lookup name when Opportunity Products are retrieved via lookup searches or advanced find
        /// 
        protected void UnpackNameOnRetrieveRelated(LocalPluginContext localContext)
        {
            IPluginExecutionContext context = localContext.PluginExecutionContext;
            Entity target = (Entity)context.OutputParameters["BusinessEntity"];
            if (target.Attributes.ContainsKey("productid"))
            {
                ((EntityReference)target["productid"]).Name = UnpackName(localContext, target.GetAttributeValue("productid").Name);
            }

        }

        /// 
        /// Unpack the product name field
        /// 
        protected string UnpackName(LocalPluginContext localContext, string name)
        {
            // Get the language of the user
            int userLanguageId = 0;
            if (localContext.PluginExecutionContext.SharedVariables.ContainsKey("UserLocaleId"))
            {
                // Get the user language from the pipeline cache
                userLanguageId = (int)localContext.PluginExecutionContext.SharedVariables["UserLocaleId"];
            }
            else
            {
                // The user language isn't cached in the pipline, so get it here
                Entity userSettings = localContext.OrganizationService.Retrieve(
                    "usersettings", 
                    localContext.PluginExecutionContext.InitiatingUserId, 
                    new ColumnSet("uilanguageid"));
                userLanguageId = userSettings.GetAttributeValue("uilanguageid");
                localContext.PluginExecutionContext.SharedVariables["uilanguageid"] = userLanguageId;
            }

            // Split the name
            string[] labels = name.Split(',');
           
            // Which language is set for the user?
            int labelIndex = Array.IndexOf(locales, userLanguageId);

            // Return the correct translation
            return labels[labelIndex];
        }
       
        
        /// 
        /// Get a value from the target if present, otherwise from the preImage
        /// 
        private T GetAttributeValue(string attributeName, Entity preImage, Entity targetImage)
        {
            if (targetImage.Contains(attributeName))
            {
                return targetImage.GetAttributeValue(attributeName);
            }
            else if (preImage != null)
                return preImage.GetAttributeValue(attributeName);
            else
                return default(T);
        }
    }
}

11) Locate the RegisterFile.crmregister file in the CRM Solution Project, and paste the following inside the PluginTypes section:
IMPORTANT: Change the Name and TypeName to match the namespace of your project.

  
<Plugin Description="Multi Language Support for Products" 
		FriendlyName="MultiLanguageProductPlugin" 
		Name="Develop1.Plugins.MultiLanguageProductPlugin" 
		Id="00000000-0000-0000-0000-000000000000"
		TypeName="Develop1.Plugins.MultiLanguageProductPlugin">
  <Steps>
    <clear />
	  <!-- Pack the translations into the name field when a Product is Created -->
    <Step CustomConfiguration="" 
		  Name="ManageNameFieldPlugin" 
		  Description="Pack the translations into the name field when a Product is Created" 
		  Id="00000000-0000-0000-0000-000000000000" 
		  MessageName="Create" 
		  Mode="Synchronous" 
		  PrimaryEntityName="product" 
		  Rank="1" 
		  SecureConfiguration="" 
		  Stage="PreInsideTransaction" 
		  SupportedDeployment="ServerOnly">
      <Images />
    </Step>
	  <!-- Pack the translations into the name field when a Product is Updated -->
    <Step CustomConfiguration=""
		  Name="ManageNameFieldPlugin" 
		  Description="Pack the translations into the name field when a Product is Updated" 
		  Id="00000000-0000-0000-0000-000000000000" 
		  MessageName="Update" 
		  Mode="Synchronous" 
		  PrimaryEntityName="product" 
		  Rank="1" 
		  SecureConfiguration="" 
		  Stage="PreInsideTransaction" 
		  SupportedDeployment="ServerOnly">
      <Images>
		  <!-- We need the translated labels even if it isn't updated in this update -->
        <Image Attributes="new_name_en,new_name_de" 
			   EntityAlias="PreImage" 
			   Id="00000000-0000-0000-0000-000000000000" 
			   MessagePropertyName="Target" 
			   ImageType="PreImage" />
      </Images>
    </Step>
	  <!-- Unpack the Product name field when Retrieved-->
    <Step CustomConfiguration="" 
		  Name="PostProductRetrieve" 
		  Description="Unpack the Product name field when Retrieved" 
		  Id="00000000-0000-0000-0000-000000000000" 
		  MessageName="Retrieve" 
		  Mode="Synchronous" 
		  PrimaryEntityName="product" 
		  Rank="1" 
		  SecureConfiguration="" 
		  Stage="PostOutsideTransaction" 
		  SupportedDeployment="ServerOnly">
      <Images />
    </Step>
	  <!-- Unpack the Product name field when Retreived in Lookup/advanced find-->
    <Step CustomConfiguration="" 
		  Name="PostProductRetrieveMultiple" 
		  Description="Unpack the Product name field when Retreived in Lookup/advanced find" 
		  Id="00000000-0000-0000-0000-000000000000" 
		  MessageName="RetrieveMultiple" 
		  Mode="Synchronous" 
		  PrimaryEntityName="product" 
		  Rank="1" 
		  SecureConfiguration="" 
		  Stage="PostOutsideTransaction" 
		  SupportedDeployment="ServerOnly">
      <Images />
    </Step>
	  <!-- Unpack the Product name in the Opportunity Product productid lookup when Retreived-->
    <Step CustomConfiguration="" 
		  Name="PostOpportunityProductRetrieve" 
		  Description=" Unpack the Product name in the Opportunity Product productid lookup when Retreived" 
		  Id="00000000-0000-0000-0000-000000000000" 
		  MessageName="Retrieve" 
		  Mode="Synchronous" 
		  PrimaryEntityName="opportunityproduct" 
		  Rank="1" 
		  SecureConfiguration="" 
		  Stage="PostOutsideTransaction" 
		  SupportedDeployment="ServerOnly">
      <Images />
    </Step>
	  <!-- Unpack the Product name in the Opportunity Product productid lookup when Retreived in Lookup/advanced find-->
    <Step CustomConfiguration="" 
		  Name="PostProductOpportunityRetrieveMultiple" 
		  Description="Post-Operation of Product Opportunity Retrieve" 
		  Id="00000000-0000-0000-0000-000000000000" 
		  MessageName="RetrieveMultiple" 
		  Mode="Synchronous" 
		  PrimaryEntityName="opportunityproduct" 
		  Rank="1" 
		  SecureConfiguration="" 
		  Stage="PostOutsideTransaction" 
		  SupportedDeployment="ServerOnly">
      <Images />
    </Step>
  </Steps>
</Plugin>

12) Build and deploy your project.

You should now be able to create products, providing both a German and English name, and see the correct translations depending on your language selection.

You can extend this solution to include quotes, orders, price lists so that the lookups to products on those entities will also show the correct translated name.

In the same way that you can export translations from a solution to be translated, you can export the products to excel and mark them as being available for re-import. This file can be passed to a translator, updated and then re-imported.

If you need multi-language support for product names in Report, you can simply ensure that all the translated name fields (new_name_en, new_name_de) are included in the query and use an Expression such as :

 

=IIF(Parameters!CRM_UILanguageId.Value=1031,Fields!new_name_de.Value,Fields!new_name_en.Value)

 

This is a proposed solution to allowing lookup names to be translated into multiple languages. I welcome any feedback/suggestions/alternatives.

Download the full solution from the MSDN Code Gallery.

Until next time! Smile

@ScottDurow

Posted on 26. April 2012

QualifyLead PlugIn

Supposing you needed some custom logic to happen immediately after a lead was qualified. You can achieve this by registering a Plug-in on the QualifyLead Post Operation stage. From within this Plug-in you can easily get a reference to the newly created Account, Contact and Opportunity and make any changes you need.

Inside the PlugIn you need the following code:

IOrganizationService service = localContext.OrganizationService;
// Get the qualified lead
EntityReference leadid = (EntityReference) localContext.PluginExecutionContext.InputParameters["LeadId"];
Lead lead = (Lead)service.Retrieve(leadid.LogicalName, leadid.Id,new ColumnSet(LeadAttributes.crm_AccountType));

// Get the newly created account, contact, opportunity
Contact contact = null;
Opportunity opportunity = null;
Account account = null;
foreach (EntityReference created in (IEnumerable<object>) localContext.PluginExecutionContext.OutputParameters["CreatedEntities"])
{
    switch (created.LogicalName)
    {
        case Contact.EntityLogicalName:
            contact = (Contact)service.Retrieve(Contact.EntityLogicalName, created.Id, new ColumnSet(true));
            break;
        case Account.EntityLogicalName:
            account = (Account)service.Retrieve(Account.EntityLogicalName, created.Id, new ColumnSet(true));
            break;
        case Opportunity.EntityLogicalName:
            opportunity = (Opportunity)service.Retrieve(Opportunity.EntityLogicalName, created.Id, new ColumnSet(true));
            break;

    }
} 

If you need to make any changes to the created records, you can simply use:

contact.LastName = "some change";
service.Update(contact);

Hope this helps.

Posted on 22. September 2011

Update Rollup 4 Released!

Here it is!

http://blogs.msdn.com/b/crm/archive/2011/09/22/update-rollup-4-for-microsoft-dynamics-crm-2011.aspx

The release schedule for the next two are:

  • Update Rollup 5 will be delivered earlier than the 8-week cycle and is scheduled to release at the end of October 2011.
  • Update Rollup 6 will be delivered on its predefined schedule, 16 weeks after Update Rollup 4, which puts the delivery in January 2012.

Some great fixes in UR4 though. I'm particularly pleased about:

  • If you register a plug-in assembly against an incident entity in the post-operation stage, the plug-in assembly is unexpectedly executed outside the transaction. 
  • Assume that you create a task in Outlook and track the task in Microsoft Dynamics CRM 2011. When you synchronize the task with Microsoft Dynamics CRM, the due date for the task is changed incorrectly to a previous due date. 
  • When you add the Actual End field to an email message form and then set the format to Date and Time, the time value is displayed in UTC format instead of your local time zone. 
  • When you click Audit History in the Microsoft Dynamics CRM 2011 Web client, you receive the following error message: An unexpected error occurred.

The incident transaction issue was causing serious problems for customers with Plugins that needed to rollback the 'create' transaction. What happened prior to UR4 is that the transaction didn't rollback if your plugin threw an exception, even though the form thought that the record hadn't be saved. When you then tried to save again, a 'duplicate record' exception was thrown.

 

 

Posted on 22. September 2011

AddItem & RemoveItem Plugin Message‘gotchas’

When registering a Plug-In Step on the 'AddItem' and 'RemoveItem' messages I've found a couple of things to watch out for.

1. Missing InputParameters

When registering on Campaign Activity in response to adding a Marketing List, the InputParameters collection on the 'AddItem' step should contain the following:

  • EntityName = 'List' (String)
  • CampaignActivityId = <Id of the activity the list is being added to> (Guid)
  • ItemId = <Id of the list being added> (Guid)

However, when registering against the 'RemoveItem' message, watch out for the fact the 'EntityName' parameter is missing.

2. AddItem message fired even if item already is added.

When registering on Campaign Activity in response to adding a Marketing List, the AddItem message will fire each time the user selects 'Add from Campaign' even if the list already is associated with the Campaign Activity.

For this reason, you will need to check that the item is not already present to prevent your plugin from executing even when the item isn't actually being added:

 

bool listAllreadyAdded = (from l in serviceContext.CreateQuery<List>()
                                         join a in serviceContext.CreateQuery<CampaignActivityItem>()
                                           on l.ListId equals a.ItemId
                                         where a.CampaignActivityId.Id == activityId
                                         select true
                                        ).FirstOrDefault();

 

Posted on 24. June 2011

SQL Queries from Transactional Plugin Pipeline

Sometimes the LINQ, Query Expressions or Fetch just doesn't give you the ability to quickly query your data in the way you want to. A good example of this is the lack of left outer join support if you want a where clause to filter results based on the joined entity. Sometime, you just need to query your database using good old T-SQL. In CRM 4 you could do this fairly easily by simply opening a connection directly and doing what you need to.

Since Transactional Pipelines were introduced with CRM2011 , I've been dancing a jig every time I don't have write manual rollback compensation code – but – if you try a SQL query from a transactional pipeline that queries the same entity that you are updating/inserting, you'll get a blocking lock that will cause the operation to time out.

To get around this, you have a number of options:

1) Call the SQL from outside a transaction in the PreValidation or an Async Pipeline

2) Use the following method to hook into the CRM Transaction and execute your query from within that.

Note: I should say that this could be considered breaking the following rule in the SDK that defines what is supported or not:

"The use of application programming interfaces (APIs) other than the documented APIs in the Web services DeploymentService, DiscoveryService, Organization Data Service, SOAP endpoint for Web Resources and OrganizationService."

I'm assuming that you are familiar with the System.Data library, so I'm just posing how to get a SqlTransaction, and you can do the rest:

 

Microsoft.Xrm.Sdk.IPluginExecutionContext context = (Microsoft.Xrm.Sdk.IPluginExecutionContext)
serviceProvider.GetService(typeof(Microsoft.Xrm.Sdk.IPluginExecutionContext));
object platformContext = context.GetType().InvokeMember("PlatformContext", System.Reflection.BindingFlags.GetProperty, null, context, null);
SqlTransaction tx = (SqlTransaction)platformContext.GetType().InvokeMember("SqlTransaction", System.Reflection.BindingFlags.GetProperty, null, platformContext, null);
DataSet result = SqlHelper.ExecuteDataset(tx, CommandType.Text, "SELECT ...");

 

You can also call a stored procedure in a different database that points back to the MSCRM database if you have complex queries. You'll need to use 'SET TRUSTWORTHY ON' to ensure that the security context is passed between the two databases.

My advice would be to only use this method only where using the SDK is just not possible or performs too slowly.

Hope this helps.

UPDATE: I've been doing some benchmarking recently on the FetchXml Reporting Services provider and using the Filtered Views. Under some circumstances where a user has only partial read access to records, using the FetchXml provider can perform considerably faster than the Filtered Views. Watch out for a blog post on this subject.

Posted on 2. December 2010

Unit Testing Dynamics CRM 2011 Pipeline Plugins using Rhino Mocks

It’s been a while since Dynamics CRM 2011 Beta 1 was released (surely we are due Beta 2 soon!) so I thought it was about time I set up a Unit Test framework for PlugIns. I’ve been using Rhino Mocks for a while to great effect, so here we go!

This example aims to unit test the SDK sample Plugins, and demonstrates the following:

    • Mocking the pipeline context, target and output property bags.
    • Mocking the Organisation Service.
    • How to assert that exceptions are raised by a plugin
    • How to assert that the correct Organisation Service method was called with the desired values.

To build the examples, you’ll need the CRM2011 SDK example plugins and Rhino Mocks 3.6 (http://ayende.com/Blog/archive/2009/09/01/rhino-mocks-3.6.aspx). 

The key principle of mocking is that we can exercise and examine the code that we need to test without executing the bits that are not being tested. By mocking we are fixing behaviour and return values of the dependant code so that we can assert if the results are what we expect.  This approach supports Test Driven Development (TDD), where the test is written first and then the desired functionality is added in order that the test passes. We can then say we are ‘Done’ when all tests pass. 

So in our example, the Followup Plugin should create a task with the regarding id set to the id of the account. So by mocking the pipeline context, we can specify the account id, and check that the resulting task that is created is regarding the same account. Using Rhino Mocks allows us to create a mock Organisation Service and assert that the Create method was called passing a task with the desired attributes set.

 

[TestMethod]
public void FollowupPlugin_CheckFollowupCreated()
{
    RhinoMocks.Logger = new TextWriterExpectationLogger(Console.Out);

    // Setup Pipeline Execution Context Stub
    var serviceProvider = MockRepository.GenerateStub();
    var pipelineContext = MockRepository.GenerateStub();
    Microsoft.Xrm.Sdk.IPluginExecutionContext context = (Microsoft.Xrm.Sdk.IPluginExecutionContext)
        serviceProvider.GetService(typeof(Microsoft.Xrm.Sdk.IPluginExecutionContext));
    serviceProvider.Stub(x => x.GetService(typeof(Microsoft.Xrm.Sdk.IPluginExecutionContext))).Return(pipelineContext);

    // Add the target entity
    ParameterCollection inputParameters = new ParameterCollection();
    inputParameters.Add("Target", new Entity("account"));
    pipelineContext.Stub(x => x.InputParameters).Return(inputParameters);

    // Add the output parameters
    ParameterCollection outputParameters = new ParameterCollection();
    Guid entityId= Guid.NewGuid();

    outputParameters.Add("id", entityId);
    pipelineContext.Stub(x => x.OutputParameters).Return(outputParameters);

    // Create mock OrganisationService
    var organizationServiceFactory = MockRepository.GenerateStub();
    serviceProvider.Stub(x => x.GetService(typeof(Microsoft.Xrm.Sdk.IOrganizationServiceFactory))).Return(organizationServiceFactory);
    var organizationService = MockRepository.GenerateMock();
    organizationServiceFactory.Stub(x => x.CreateOrganizationService(Guid.Empty)).Return(organizationService);

            
    // Execute Plugin
    FollowupPlugin plugin = new FollowupPlugin();
    plugin.Execute(serviceProvider);

    // Assert the task was created
    organizationService.AssertWasCalled(x => x.Create(Arg.Is.NotNull));

    organizationService.AssertWasCalled(x => x.Create(Arg.Matches(s => 
            ((EntityReference)s.Attributes["regardingobjectid"]).Id.ToString() == entityId.ToString()
            &&
            s.Attributes["subject"].ToString() == "Send e-mail to the new customer."
            )));
          
}

The key thing to notice is that the only mock object here is the OrganisationService - all others are stubs. The difference between a stub and a mock is that the mock records the calls that are made so that they can be verified after the test has been run. In this case we are verifying that the Create method was called with the correct properties set on the task entity.

It’s worth noting the RhinoMocks.Logger assignment. This gives the Rhino logging output in the VS2010 test results; most helpful during debugging asserts that don’t do as you expect.

Looking at the sample Account Plugin, it throws an exception when the account number is already set - so what testing that plugin’s throw exceptions under some conditions. Unfortunately, the standard VS2011 ExpectedExceptionAttribute doesn’t provide the functionality we need here since we can’t check exception attributes, nor can we run the tests in Debug without the Debugger breaking into the code even though the exception is marked as being expected. In order to get around this this I use a class written by Keith E. Burnell:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Kb.Research.RhinoMocks.UnitTests.CusomAssertions
{
    /// 
    /// Custom assertion class for unit testing expected exceptions.  
    /// A replacement for the ExpectedException attribute in MSTest
    /// 
    public static class AssertException
    {
        #region Methods

        /// 
        /// Validates that the supplied delegate throws an exception of the supplied type
        /// 
        /// Type of exception that is expected to be thrown
        /// Delegate that is expected to throw an exception of type 
        public static void Throws<TExpectedExceptionType>(Action actionThatThrows) where TExpectedExceptionType : Exception
        {
            try
            {
                actionThatThrows();
            }
            catch (Exception ex)
            {
                Assert.IsInstanceOfType(ex, typeof(TExpectedExceptionType), String.Format("Expected exception of type {0} but exception of type {1} was thrown.", typeof(TExpectedExceptionType), ex.GetType()));
                return;
            }
            Assert.Fail(String.Format("Expected exception of type {0} but no exception was thrown.", typeof(TExpectedExceptionType)));
        }

        /// 
        /// Validates that the supplied delegate throws an exception of the supplied type
        /// 
        /// Type of exception that is expected to be thrown
        /// Expected message that will be included in the thrown exception
        /// Delegate that is expected to throw an exception of type 
        public static void Throws<TExpectedExceptionType>(string expectedMessage, Action actionThatThrows) where TExpectedExceptionType : Exception
        {
            try
            {
                actionThatThrows();
            }
            catch (Exception ex)
            {
                Assert.IsInstanceOfType(ex, typeof(TExpectedExceptionType), String.Format("Expected exception of type {0} but exception of type {1} was thrown.", typeof(TExpectedExceptionType), ex.GetType()));
                Assert.AreEqual(expectedMessage, ex.Message, String.Format("Expected exception with message '{0}' but exception with message '{1}' was thrown.", ex.Message, expectedMessage));
                return;
            }
            Assert.Fail(String.Format("Expected exception of type {0} but no exception was thrown.", typeof(TExpectedExceptionType)));
        }

        #endregion

    }
}

So, we want to test that if the account number is already set on execution of the AccountNumberPlugin, then an exception is raised:

[TestMethod]
public void AccountNumberPlugin_CheckExceptionIfAccountNumberSetAllready()
{
    // Setup Pipeline Execution Context Stub
    var serviceProvider = MockRepository.GenerateStub();
    var pipelineContext = MockRepository.GenerateStub();
    Microsoft.Xrm.Sdk.IPluginExecutionContext context = (Microsoft.Xrm.Sdk.IPluginExecutionContext)
        serviceProvider.GetService(typeof(Microsoft.Xrm.Sdk.IPluginExecutionContext));
    serviceProvider.Stub(x => x.GetService(typeof(Microsoft.Xrm.Sdk.IPluginExecutionContext))).Return(pipelineContext);

    // Add the target entity
    ParameterCollection inputParameters = new ParameterCollection();
    inputParameters.Add("Target",new Entity("account") { Attributes = new AttributeCollection  { 
            new KeyValuePair("accountnumber", "123")
        }});

    pipelineContext.Stub(x => x.InputParameters).Return(inputParameters);

    // Test that an exception is thrown if the account number already exists
    AccountNumberPlugin plugin = new AccountNumberPlugin();
    AssertException.Throws < InvalidPluginExecutionException> ("The account number can only be set by the system.",(Action) delegate {
            plugin.Execute(serviceProvider);
            });

}

The examples above show how to test plugins that don’t call any external services or code - where all dependencies are discovered via the execution context. Next time I’ll provide an example of how to mock an external service using inversion of control.

 

 

Posted on 11. November 2010

Export Dynamics CRM Plugin Assembly

Have you ever uploaded a Dynamics CRM Plug in and then later wanted to get the assembly that was uploaded? The following steps are to modify the Plugin Registration Tool found in the CRM SDK to allow export of assemblies once they are uploaded.

1. In the file OrganisationHelper.cs

Modify the GetColumnSet method

case PluginAssembly.EntityLogicalName:
// Develop1:Download Assembly Support
cols.AddColumns("name", "createdon", "modifiedon", "customizationlevel", "pluginassemblyid", "sourcetype", "path", "version", "publickeytoken", "culture", "isolationmode", "description","content");

2. In the file CrmPluginAssembly.cs

Add the following code to the class:

// Develop1
private string _content;
public void ExportAssembly(string path)
{
  byte[] assembly = Convert.FromBase64String(this._content);
  File.WriteAllBytes(path, assembly);
}

3. In the file CrmPluginAssembly.cs

Add the following code at the end of the RefreshFromPluginAssembly method:

// Develop1:Download Assembly Support
if (assembly.Content != null)
{
    this._content = assembly.Content;
}

4. On the PluginRegistrationForm Form

Add a button next to the Load Assembly command button with the following properties:

Text: Export Assembly

Name: cmdExport

5. Double click on the button to get to the event code:

        // Develop1:Download Assembly Support
        private void cmdExport_Click(object sender, EventArgs e)
        {
            this.m_currentAssembly.ExportAssembly(txtAssemblyPath.Text);
          MessageBox.Show("Assembly exported.");
        }

To use, you'll need to double click on a plugin assembly to bring up the properties page. Enter a file path (e.g. C:\ExportedAssembly.dll) Ddon't use the browse button since this wlll load an assembly. Click 'Export Assembly' to export your assembly.