Best practice Check

Content

Introduction

KPI provides a list of violated behaviors within your systems that go against Microsoft's or industry best practices. It covers wide range of areas like coding, deployment, customizations, configurations, and more.

  • KPI provides a list of potential issues, rated from low to high risks.

  • Choose a component by system and type, then use the source link for checks or make adjustments.

  • Click the "Help" icon for Microsoft guidelines or our industry best practice recommendations.

Best-practice Rules

The "Best-practice Check" feature evaluates all system components and categorizes any violated practices into three levels of risk:

  1. High

  2. Medium

  3. Low

Step by Step Instructions

Step 1 - Open Best Practice Violations List:

  • Click on the “Best-Practices” in the navigation bar to display a list of violated behaviors.

  • Then, click on any desired suggestion to view more details.

  • This example below highlights the issue 'Stateful Plugin' in 'BadAccountPlugin'.

Step 2 - Check Issue Description Details

Once you click on a suggestion, a detailed description of the issue will drop down. In this example, the description suggests: "Plugins should be stateless. Remove all instance properties and variables - BadAccountPlugin.cs 13:19”

Step 3 - Identify “Risk”, “System” and “Component”

From the suggestion list, users can identify additional information about the “Risk” level, “System”, and “Component” type.

  • Risk: Severity of the problem. In example above, it requires immediate attention

  • System: In which system where the problem occurred. In this case, it’s “Golet.Ketion”.

  • Component: The specific component involved. In this case, it’s the class “BadAccountPlugin”.

Step 4 - Further Investigation and Recommendations

  • Click the "<>" icon to view the source code or component details.

  • Click the “?” icon in the "Help" column to access KPI documentation with for about 40 different violation types

‘<>’ Icon

When clicking the '<>' icon, a new tab displaying the component's code is opened. In the example below, it highlights an instance property named ‘MyProperty’ in row 13.

“?” Icon

When clicking on the “?” icon, users are redirected to the KPI documentation, which offers suggested solutions

In the example below, it provides a similar case and recommended action for handling Stateful plugins See more: Best practice Check | Stateful Plugins

Explore all the Best Practice Violations listed below to gain insights into the various problems KPI can identify.

Global Best Practice Violations

Early Bound Entities (No classes detected)

A shared domain project, including all domain classes (strong types for the Dataverse data model), should be referenced by other projects.

Issue: Could not detect classes for early bound entities.

namespace Golet.Plugins.Accounts {
    public class VeryBadAccountPlugin : IPlugin {

        public void Execute(IServiceProvider serviceProvider) {

            IOrganizationService service = serviceProvider.GetService(typeof(IOrganizationService));
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            cintext = GetContext(serviceProvider);
            Entity target = (Entity)context.InputParameters["Target"];

            service.Update(target);
       }
   }
}

Solution: Enable early bound entities

namespace Domain
{
    ///<summary>Prospect or potential sales opportunity. Leads are converted into accounts, contacts, or opportunities when they are qualified. Otherwise, they are deleted or archived. (Lead)</summary>
    [System.Runtime.Serialization.DataContractAttribute]
    [Microsoft.Xrm.Sdk.Client.EntityLogicalNameAttribute("lead")]
    [System.CodeDom.Compiler.GeneratedCodeAttribute("KuppCodeAnalytics", "1.2")]
    public partial class Lead : Microsoft.Xrm.Sdk.Entity, System.ComponentModel.INotifyPropertyChanging, System.ComponentModel.INotifyPropertyChanged
    {
        public Lead() : base(EntityLogicalName)
        {
        }

        public Lead(Guid id) : base(EntityLogicalName, id)
        {
        }

        public const string EntityLogicalName = "lead";
        public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
        public event System.ComponentModel.PropertyChangingEventHandler PropertyChanging;
        private void OnPropertyChanging(string propertyName) => this.PropertyChanging?.Invoke(this, new System.ComponentModel.PropertyChangingEventArgs(propertyName));
        private void OnPropertyChanged(string propertyName) => this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
        ///<summary>Unique identifier of the account with which the lead is associated. (Lead / Account)</summary>
        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("accountid")]
        public EntityReference AccountId { get => this.GetAttributeValue<EntityReference>("accountid"); }

        ///<summary>Choose the campaign that the lead was generated from to track the effectiveness of marketing campaigns and identify  communications received by the lead. (Lead / Source Campaign)</summary>
        [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("campaignid")]

Early Bound Entities (Replace with late bound)

Avoid using the entity base class due to the high risk of misspelled names. Strong types are more robust and easier to use.

Issue: Late bound entities should be replaced with early bound entities.

Entity target = new Entity("lead");
target["leadId"] = Guid.Empty;

Solution: Use a strong typed class instead

Lead target = new Lead(Guid.Empty);

Benefits of Early-bound Entities:

  • Entity, attribute, and relationship names are verified at compile time.

  • Enhanced IntelliSense support.

  • Reduced code writing; results in more readable code

More information on late-bound and early bound entities : https://learn.microsoft.com/en-us/power-apps/developer/data-platform/org-service/early-bound-programming


ColumnSet Parameter

Retrieving all columns has a bad performance impact.

Issue: Specify the required columns instead of retrieving all columns.

Problematic implementation

var account = service.Retrieve("account", Guid.Empty, new ColumnSet(true));

Solution: Specify the required columns.

How to implement

var account = service.Retrieve("account", Guid.Empty, new ColumnSet("accountnumber"));

The ColumnSet class has an ColumnSet.AllColumns property which specifies that all columns of the table should be returned. As a performance best practice, you should not use this for production code.

ColumnSet Class: https://learn.microsoft.com/en-us/dotnet/api/microsoft.xrm.sdk.query.columnset

ColumnSet.AllColumns :https://learn.microsoft.com/en-us/dotnet/api/microsoft.xrm.sdk.query.columnset.allcolumns#microsoft-xrm-sdk-query-columnset-allcolumns

More information on ColumnSet parameter: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/best-practices/work-with-data/retrieve-specific-columns-entity-via-query-apis


Update Columns

Too many attributes can cause unexpected behavior of custom logic.

Issue: Attribute collection should only include the changed attributes. Create a new entity for the update.

var contact = service.Retrieve("contact", new Guid(), new ColumnSet(true));
contact["firstname"] = "Joe"
service.Update(contact);

Solution: Create a new instance or retrieve only the required attributes.

var contact = service.Retrieve("contact", new Guid(), new ColumnSet(true)).ToEntity<Contact>;
var updateContact = new Contact(contact.Id);
contact["firstname"] = "Joe"
service.Update(contact);


Unknown Entity

Invalid entity names will lead to exceptions. Make sure that the name is correctly spelled.

Issue: Entity logicalname doesn't exist.

var entity = new Entity(“Account_Typo”);

Solution: Use the correct name. To get IntelliSense for late-bound entities check out CA IntelliSense .NET | Entity Names

var entity = new Entity(“account”);


Unknown Attribute

Invalid attribute names will lead to exceptions. Make sure that the name is correctly spelled.

Issue: Attribute logicalname doesn't exist

var entity = new Entity("account");
entity["paymenttermscodeXXX"] = null

Solution: Use the correct name. To get IntelliSense for late-bound entity attributs check out CA IntelliSense .NET | Attributes

var entity = new Entity("account"");
entiy["paymenttersmcode"] = null;


Invalid Relation

Issue: Value doesn't match with expected relationship

var accountEntity = new Entity("account");
var orderEntity = new Entity("order");
accountEntity[primarycontactid] = orderEntity;

Solution: Make sure to use the right value for the expected relationship. To get IntelliSense for late-bound entity relationships, check out CA IntelliSense .NET | Relations

var accountEntity = new Entity("account");
var contactEntity = new Entity("contact");
accountEntity[primarycontactid] = contactEntity.Id;

Invalid Attribute Value

Issue: Value does not match with the expected type

var entity = new Entity("account");
entity["Address1_County"] = new Guid();

Solution: Make sure to use the right format for the attribute. To get IntelliSense for late-bound entity attribute value check out CA IntelliSense .NET | Attribute Values

var entity = new Entity("account");
entity["address1_country"] = "United States";

Publisher Prefix (new)

Issue: Components should not have the default publisher prefix 'new'.

The publisher record contains a Prefix value. The default value of this prefix is “new”. When you create new solution components, this prefix will be appended to the name. This is a quick way that allows people to understand what solution the components are part of.

Solution: Make sure that all of your components are created in a solution. They will have publisher prefix of the solution.

Change the solution publisher prefix

  1. Go to Settings > Customizations.

  2. Select Publishers.

  3. If there is more than one publisher, open the one with the Display Name that starts with Default Publisher for <your organization name>.

  4. At the bottom of the form, update the Prefix field to change the default value of “new” to something that identifies your organization.

  5. When you change the value, make sure to tab to the next field. The Option Value Prefix will automatically generate a number based on the customization prefix. This number is used when you add options to option sets and provides an indicator of which solution was used to add the option.

More information on publisher prefix: https://learn.microsoft.com/en-us/dynamics365/customerengagement/on-premises/customize/change-solution-publisher-prefix?view=op-9-1


Managed Publisher Prefix used for unmanaged solution

Issue: Managed prefixes from 3rd solutions should not be used for unmanged customizations.

Solution: Create a new different prefix for customizations.

Create a solution publisher

Label

Description

Display Name

The name that to display in the Publisher lookup field in the solution.

Name

Dynamics 365 Customer Engagement (on-premises) generate a unique name is based on the Display Name. The unique name can only contain alphanumeric characters and the underscore character. Note: You use the Unique Name to uniquely identify a Publisher. Managed solutions that share the same publisher can update each other.

Description

Use this field to include any relevant details about your solution.

Prefix

The customization prefix helps you identify which publisher added a solution component. For example the prefix is added to the logical name of any entities or attributes created in the context of a solution associated with this publisher. The prefix must be between two and eight characters long, and can contain only alphanumeric characters. It cannot start with 'mscrm'.

Option Value Prefix

This value lets you help separate options that you add to option sets to support merging options. A value is auto-generated based on the Prefix text to help make it more unique. The value must be between 10,000 and 99,999.

More information: https://learn.microsoft.com/en-us/dynamics365/customerengagement/on-premises/developer/understand-managed-solutions-merged?view=op-9-1#BKMK_MergingOptionSetOptions

Contact Details

Use these fields to add information that will enable people who install the solution to contact you.

More information: https://learn.microsoft.com/en-us/dynamics365/customerengagement/on-premises/developer/create-export-import-unmanaged-solution?view=op-9-1


Reference to DateTime.Now

Issue: Use DateTime.UtcNow instead, local time zone can be different in cloud environments.

Problematic implementation

//Your code in New York (Eastern Time Zone)
var createdOn = DateTime.Now;
Console.WriteLine("CreatedOn: " + createdOn);
//Output: CreatedOn: 2023-11-22 15:00:00

//Other users code in Los Angeles (Pacific Time Zone)
var createdOn = DateTime.Now;
Console.WriteLine("CreatedOn: " + createdOn);
//Output: CreatedOn: 2023-11-22 12:00:00

Solution: Always Use DateTime.UtcNow;

Hot to implement

var createdOn = DateTime.UtcNow;

More information: https://learn.microsoft.com/en-us/dotnet/api/system.datetime.utcnow?view=net-8.0 | https://learn.microsoft.com/en-us/dotnet/api/system.datetime.now?view=net-8.0


Unfiltered service endpoint step registration

Issue: An update step registration should be filtered by the relevant attributes.

If no filtering attributes are set for a service endpoint registration step, then the service endpoint will execute every time an update message occurs for that event. A combination of no filtering attributes and auto-save functionality could lead to unnecessary plug-in executions causing undesirable behavior and degrade performance.

Solution: Maximize efficiency by refining service endpoint registrations for entity updates. Target specific attribute changes to reduce unnecessary processing time. Incorporate attribute filtering in plug-in step registrations for quicker, more focused logic execution.


Invalid .NET Framework version

Issue: Project does not have the correct target version

Solution : A project with plugins or code activities should target version 4.6.2.


Source control alignment

Issue: A custom assembly was detected that is not present in your DevOps repository

Solution: Make sure that all of your custom assemblies in Dataverse are also in your DevOps repository.


Component specific Best Practice Violations

Unfiltered plugin step registration

Issue: An update step registration should be filtered by the relevant attributes.

If no filtering attributes are set for a plug-in registration step, then the plug-in will execute every time an update message occurs for that event. A combination of no filtering attributes and auto-save functionality could lead to unnecessary plug-in executions causing undesirable behavior and degrade performance.

Solution: Maximize efficiency by refining plug-in registrations for entity updates. Target specific attribute changes to reduce unnecessary processing time. Incorporate attribute filtering in plug-in step registrations for quicker, more focused logic execution.

More information plugin step registration: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/best-practices/business-logic/include-filtering-attributes-plugin-registration


Plugin is not registered

Issue: Plugin code is not registered.

All plugins need to be deployed to the Dataverse.

Plugin code should be registered or deleted if not needed.

When an assembly is uploaded, it's stored in the PluginAssembly table. Most of the properties are set using reflection of the imported assembly. The base64 encoded bytes of the assembly are stored in the Content column. While viewing the Properties of the assembly in the PRT, you can only edit the Description value. All compiled classes within the assembly that implement the IPlugin Interface or derive from CodeActivity Class are automatically registered.

IPlugin Interface: https://learn.microsoft.com/en-us/dotnet/api/microsoft.xrm.sdk.iplugin?view=dataverse-sdk-latest

CodeActivity Class: https://learn.microsoft.com/en-us/dotnet/api/system.activities.codeactivity?view=netframework-4.8.1

More information on Plugin registration: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/register-plug-in


Target Update

Issue: The target entity should not be updated via the IOrganizationService.

Problematic Implementation

namespace Golet.Plugins.Accounts {
    public class VeryBadAccountPlugin : IPlugin {

        public void Execute(IServiceProvider serviceProvider) {

            ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            IOrganizationService service = serviceProvider.GetService(typeof(IOrganizationService)) as IOrganizationService;

            Entity target = (Entity)context.InputParameters["Target"];

            service.Update(target);
       }
   }
}

Solution: Remove the update statement.

The recommended way to update the plugin target entity is to modify the attributes in the preoperation or prevalidation stage without an explicit update call because the values will be saved automatically in the main operation.

namespace Golet.Plugins.Accounts {
    public class VeryBadAccountPlugin : IPlugin {

        public void Execute(IServiceProvider serviceProvider) {

            ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            IOrganizationService service = serviceProvider.GetService(typeof(IOrganizationService)) as IOrganizationService;

            Entity target = (Entity)context.InputParameters["Target"];
       }
   }
}

Missing plugin base class

A custom base class should handle the call delegation based on the plugin context information.

Issue: The IPlugin interface should not be used directly.

Problematic Implementation

public class AccountPlugin : IPlugin {
        public void Execute(IServiceProvider serviceProvider) {

            IOrganizationService service = (IOrganizationService)serviceProvider.GetService(typeof(IOrganizationService));
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            if(context.Mode == 1 && context.Stage == 20) {
                //something
            }

Solution: Handle the common logic in a base class. 

How to implement

[CrmStep(Account. EntityLogicalName, CrmMessages.Create, CrmStages.PreOperation, false, false)]
public void CreatePreOperation(IServiceProvider provider) { 
    PluginContext<Account> context = this.CreateContext<Account>(provider); 

Stateful Plugins

Instances of plugin classes will be recreated based on the platform requirements, there is no guarantee that instances are being reused. Data bounded to an instance is mostly not thread-safe.

Issue: Plugins should be stateless. Remove all instance properties and variables.

Problematic Implementation

public class AccountPlugin : IPlugin {
        private IOrganizationService service;
        public void Execute(IServiceProvider serviceProvider){

Solution: Remove the instance variable.

How to implement

public class AccountPlugin : IPlugin {
        public void Execute(IServiceProvider serviceProvider) {

            IOrganizationService service = (IOrganizationService)serviceProvider.GetService(typeof(IOrganizationService));
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));


Parallel SDK Call

Issue: Multi-threading or parallel calls within plug-ins or custom workflow activities can cause unexpected side effects.

Multi-threading or parallel calls within plug-ins or custom workflow activities can cause corruption of those the connections. As an example, executing parallel threads can log exceptions such as:

Generic SQL error. The transaction active in this session has been committed or aborted by another session.

Also, non-thread safe objects such as items in the System.Collection.Namespace can become corrupted by parallel threads.

Problematic Implementation

namespace Golet.Plugins.Accounts {
    internal class BadPatternLogic {

        public void RunParallel(IOrganizationService service) {

            var months = Enumerable.Range(1, 12).Select(e => new DateTime(DateTime.Now.Year, e, 1));

            Parallel.ForEach(months, e => service.Create(new Entity("task") { }));
        }

Solution: Make parallel calls synchronous and register plugin step asynchronous.

More information parallel SDK calls : https://learn.microsoft.com/en-us/power-apps/developer/data-platform/best-practices/business-logic/do-not-use-parallel-execution-in-plug-ins


Resource intensive API

Issue: Due to limitations in Dataverse resource-intensive API could lead to reaching the resource limit.

namespace Golet.Plugins.Accounts {
    internal class BadPatternLogic {

        public void GenerateSomeImages() {
            // Create a 4K image with 3840 x 2160 pixels, 32 bits per pixel, and 72 dpi resolution
            Bitmap image = new Bitmap(3840, 2160, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
            image.SetResolution(72, 72);

Sollution: This operation should be migrated to a different environment.


Batch request

Issue: Do not use batch request within a plugin context.

Due to their long-running nature, using ExecuteMultipleRequest class or ExecuteTransactionRequest class request classes within the context of a plug-in or workflow activity expose sandbox-isolated plug-in types to the two-minute (120000ms) channel timeout exception and can degrade the user experience for synchronous registrations.

ExecuteMultipleRequest: https://learn.microsoft.com/en-us/dotnet/api/microsoft.xrm.sdk.messages.executemultiplerequest

ExecuteTransactionRequest : https://learn.microsoft.com/en-us/dotnet/api/microsoft.xrm.sdk.messages.executetransactionrequest

Problematic Implementation

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

        QueryExpression query = new QueryExpression("account")
        {
            ColumnSet = new ColumnSet("accountname", "createdon"),
        };

        //Obtain the results of previous QueryExpression
        EntityCollection results = service.RetrieveMultiple(query);

        if (results != null && results.Entities != null && results.Entities.Count > 0)
        {
            ExecuteMultipleRequest batch = new ExecuteMultipleRequest();
            foreach (Entity account in results.Entities)
            {
                account.Attributes["accountname"] += "_UPDATED";

                UpdateRequest updateRequest = new UpdateRequest();
                updateRequest.Target = account;

                batch.Requests.Add(updateRequest);
            }

            service.Execute(batch);
        }
        else return;

    }
}

Solution: Submit each request directly instead of batching them and submitting as a single request.

How to implement

foreach (request in requests) service.Execute(request)

Use these batch messages where code is being executed outside of the platform execution pipeline, such as integration scenarios where network latency would likely reduce the throughput and increase the duration of larger, bulk operations.

More specifically, use them in the following scenarios:

  • Use execute multiple request to bulk load data or external processes that are intentional about executing long-running operations (greater than two minutes).

  • Use execute multiple request to minimize the round trips between custom client and Dynamics 365 servers, thereby reducing the cumulative latency incurred.

  • Use execute transaction request for external clients that require the batch of operations to be committed as a single, atomic database transaction or rollback if any exception is encountered. Be aware of the potential for database blocking for the duration of the long-running transaction.

More information's: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/best-practices/business-logic/avoid-batch-requests-plugin


Recursive plugin execution

Issue: There is a risk to reach the max plugin depth level.

Problematic Implementation

namespace Golet.Plugins.Accounts {
    public class BadAccountPlugin : PluginBase {

        public int MyProperty { get; set; }

        protected override void ExecuteInternal(PluginContext context) { 

            var target = context.GetTarget<Account>();
            target.Name = "New Name";
            context.Service.Update(target);

Another example: https://learn.microsoft.com/en-us/troubleshoot/power-platform/power-apps/dataverse/dataverse-plug-ins-errors#example

Solution: Analyze the code and the component to avoid infinite loops.


Default Plugin Assembly Version

Issue: Assembly versions should be updated during the deployment.

Solution: Make sure that in the deployment pipeline in Dataverse the Assembly Version is updated.

// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version
//      Build Number
//      Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.2.0.0")]
[assembly: AssemblyFileVersion("1.2.0.0")]

Step registration for Retrieve and RetrieveMultiple

Issue: These registrations can have a significant impact on the overall performance of the system. Try to avoid Retrieve/RetrieveMultiple steps where possible.

The Retrieve and RetrieveMultiple messages are two of the most frequently processed messages. The Retrieve message is triggered when opening up an entity form or when an entity is being accessed using the Retrieve operation in one of the service endpoints. RetrieveMultiple is triggered due to various actions in the application and service endpoints, for example, when populating a grid in the user interface. Adding synchronous plug-in logic to these message events can cause

  • Unresponsive model-driven apps

  • Slow client interactions

  • The browser stops responding

Solution: Avoid Retrieve/RetrieveMultiple steps. If it is an appropriate solution, then follow these tips to minimize the impact to the environment:

  • Include conditions in the plug-in code to quickly check if the targeted logic needs to be performed. If it does not, then return quickly, refraining from executing unnecessary extra steps that will delay returning the data to the caller.

  • Avoid including long running tasks, especially those that can be non-deterministic, such as the invocation of external service calls or complex queries to Dynamics 365.

  • Limit or avoid querying for additional data from Microsoft Dataverse

More information: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/best-practices/business-logic/limit-registration-plugins-retrieve-retrievemultiple


Source control alignment

Issue: A custom webresources was found that is not included in your DevOps repository.

Solution: Make sure that all of your webresources in Dataverse are also in your DevOps repository.


Static entity reference in cloud flow

Issue: If static entity references are used in a cloud flow and then deployed to a test environment, a problem may arise due to the differing IDs between the development and test environments.

Problematic Implementation

Solution: Cloud Flows should not contain static entity references.


Static entity reference in workflow

Issue: If static entity references are used in a workflow and then deployed to a test environment, an issue may arise because the IDs in the development environment and test environment will differ

Problematic Implementation

Solution: Workflow should not contain static entity references.


Static Guid usage

Issue: Static guids can lead to severe problems when deploying to a different environment.

Problematic implementation

Entity e = result.Entities.First();
e.Id = Guid.Parse("a4c598c8-5083-4ac8-82d0-1749206d9b93");
e.Id = new Guid("a4c598c8-5083-4ac8-82d0-1749206d9b93");

Solution: Make sure always to use the best-practice solutions and never use static guids.


Duplicated plugin step registration

Issue: Delayed processing of asynchronous jobs when registered or degraded user performance experience when registered as a synchronous execution mode.

  • Delayed processing of asynchronous jobs when registered as an asynchronous execution mode.

  • Degraded user performance experience when registered as a synchronous execution mode. Experiences include:

    • Unresponsive model-driven apps

    • Slow client interactions

    • The browser stops responding

Duplicate plug-in step registration could cause SQL deadlocking when the events are registered on an update message. When issuing an update on a record, SQL will create a row lock on that record. If another transaction tries to update that same record, it will have to wait until the lock is released before it's able to make the update. If a timeout occurs, the transaction is rolled back and the update is not committed to the SQL database.

Solution: Ensure you are updating existing plugin registration steps rather than deleting and recreating them. Additionally, only create and update plug-in registration steps in a supported manner.

More information: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/best-practices/business-logic/do-not-duplicate-plugin-step-registration


KeepAlive when interacting with external calls

Issue: If a plugin executing external web requests attempts to use KeepAlive on a closed connection, it will fail to execute the web request. Having the KeepAlive property set to true in the HTTP request header, or not explicitly defined as false, can lead to increased execution times for plugins

WebRequest (Problematic)

WebRequest request = WebRequest.Create("https://www.demo.com/api/stuff");
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
response.Close();

HttpClient (Problematic)

HttpClient client = new HttpClient()
HttpResponseMessage response =  client.GetAsync(webAddress).GetAwaiter().GetResult();

WebClient (Problematic)

Microsoft does not recommend that the WebClient class is used for new development. Instead, use the System.Net.Http.HttpClient.

WebClient client = new WebClient();
var readResult = client.OpenRead("https://demo/test");

Solution: Make sure to set KeepAlive to false when interacting with external calls.

WebRequest (How to implement)

WebRequest request = WebRequest.Create("https://www.demo.com/api/stuff");
reqest.KeepAlive = false;
HttpWebResponse response = (HttpWebResponse)request.GetResponse();

HttpClient (How to implement)

First solution to set ‘Connection’ to closed not keep-alive.

HttpClient client = new HttpClient()
client.DefaultRequestHeaders.ConnectionClose = true;
HttpResponseMessage response =  client.GetAsync(webAddress).GetAwaiter().GetResult();

Second solution to set ‘Connection’ to closed and not keep-alive.

HttpClient client = new HttpClient()
client.DefaultRequestHeaders.Connection.Add("close");
HttpResponseMessage response =  client.GetAsync(webAddress).GetAwaiter().GetResult();

WebClient (How to implement)

Microsoft does not recommend that the WebClient class is used for new development. Instead, use the System.Net.Http.HttpClient.

public class _WebClient : WebClient {
            // Overrides the GetWebRequest method and sets keep alive to false
            protected override WebRequest GetWebRequest(Uri address) {
                HttpWebRequest req = (HttpWebRequest)base.GetWebRequest(address);
                req.KeepAlive = false;
                return req;
            }
        }
_WebClient client = new _WebClient();
var readResult = client.OpenRead("https://demo/test");

More information: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/best-practices/business-logic/set-keepalive-false-interacting-external-hosts-plugin


InvalidPluginExecutionException in plugin

Issue: If a synchronous plugin returns an exception other than InvalidPluginExecutionException back to the platform, it results in an error display in a Power Apps client, leading to a user-unfriendly experience.

If a synchronous plugin returns an exception other than InvalidPluginExecutionException back to the platform, in a Power Apps client an error is displayed to the user with the message of the exception Message and the stack trace. This provides an unfriendly user experience in what is likely already a frustrating situation.

If you are using InvalidPluginExecutionException to intentionally cancel the operation because of data validation logic issue, you should provide guidance applicable to the application user so that they can correct the issue and continue.

If the error is unexpected, it is still recommended to catch the exception, convert it into a InvalidPluginExecutionException, and then throw the new exception so that applications can show a friendly error message with guidance to help a user or technical staff quickly identify the issue.

InvalidPluginExecutionException: https://learn.microsoft.com/en-us/dotnet/api/microsoft.xrm.sdk.invalidpluginexecutionexception

Solution: Plugins should only return an InvalidPluginExecutionException for the following reasons:

  • Show a useful message to the user

  • Avoiding event log/trace file bloat

catch(Exception ex)
{
  throw new InvalidPluginExecutionException("Error...");
}

This message will be displayed as a Business Process Error pop-up window.

More information: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/best-practices/business-logic/use-invalidpluginexecutionexception-plugin-workflow-activities | https://learn.microsoft.com/en-us/power-apps/developer/data-platform/handle-exceptions


ITracingService in plugin

Issue: Not using the ITracingService in plugin development can make diagnosing issues and debugging significantly harder.

The ITracingService interface assists developers by recording run-time custom information as an aid in diagnosing the cause of code failures or unexpected behavior in plug-ins. Before writing to the tracing service, you must first extract the tracing service object from the passed execution context. Afterwards, simply add Trace calls to your custom code where appropriate passing any relevant diagnostic information in that method call.

Solution: Use the ITracingService within your plugins to gain insight into what happens when your code runs.

How to implement

ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));

tracingService.Trace("AdvancedPlugin: Verifying the client is not offline.");

Tracing is especially useful to troubleshoot registered custom code as it is the only supported troubleshooting method for that scenario. Tracing is supported for sandboxed (partial trust) and full trust registered custom code and during synchronous or asynchronous execution. Tracing isn’t supported for custom code that executes in Microsoft Dynamics 365 for Outlook or other mobile client.

Trace logging using ITracingService interface works only when the plug-in is registered in Sandbox mode and you must enable trace logging to get run-time data. For more information see: https://learn.microsoft.com/en-us/dynamics365/customerengagement/on-premises/developer/debug-plugin?view=op-9-1#logging-and-tracing

More information: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/best-practices/business-logic/use-itracingservice-plugins


Timeout when making external calls

Issue: If a plugin makes external web requests that fail to respond quickly, the plugin will wait for the full default timeout period before failing. This duration may cause a long transaction that can effect other operations.

If the plug-in is registered:

  • Synchronously, users may experience:

    • Unresponsive model-driven apps

    • Slow client interactions

    • The browser stops responding

  • Asynchronously, plug-in executions may take an extended period of time before failing.

Solution: Establish an expected baseline time that a calling service will respond. The longer it exceeds this normal response time, the higher the probability it will ultimately fail.

If you are using System.Net.Http.HttpClient Class, you can set the Timeout value explicitly, as shown in this example setting the timeout to 15 seconds.

System.Net.Http.HttpClient: https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient

HttpClient client = new HttpClient()
client.Timeout = TimeSpan.FromMilliseconds(15000);
client.DefaultRequestHeaders.ConnectionClose = true;
HttpResponseMessage response =  client.GetAsync(webAddress).GetAwaiter().GetResult();

If you are using System.Net.WebClient Class, you need to create a derived class and override the base GetWebRequest Method to set the timeout:

System.Net.WebClient: https://learn.microsoft.com/en-us/dotnet/api/system.net.webclient

GetWebRequest Method: https://learn.microsoft.com/en-us/dotnet/api/system.net.webclient.getwebrequest

Microsoft does not recommend that the WebClient class is used for new development. Instead, use the System.Net.Http.HttpClient.

  /// <summary>
  /// A class derived from WebClient with 15 second timeout and KeepAlive disabled
  /// </summary>
  public class CustomWebClient : WebClient
  {
    protected override WebRequest GetWebRequest(Uri address)
    {
      HttpWebRequest request = (HttpWebRequest)base.GetWebRequest(address);
      if (request != null)
      {
        request.Timeout = 15000; //15 Seconds
        request.KeepAlive = false;
      }
      return request;
    }
  }

Then you can use this class in your plugin code:

using (CustomWebClient client = new CustomWebClient())
{
  byte[] responseBytes = client.DownloadData(webAddress);
  string response = Encoding.UTF8.GetString(responseBytes);
  tracingService.Trace(response);
  //Log success in the Plugin Trace Log:
  tracingService.Trace("WebClientPlugin completed successfully.");
}

More information: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/best-practices/business-logic/set-timeout-for-external-calls-from-plug-ins


Need help? Contact support