Best Practices Analytics
Content
- Introduction
- Best-practice Rules
- Step by Step Instructions
- Global Best Practice Violations
- Early Bound Entities (No classes detected)
- Early Bound Entities (Replace with late bound)
- ColumnSet Parameter
- Update Columns
- Unknown Entity
- Unknown Attribute
- Invalid Relation
- Invalid Attribute Value
- Publisher Prefix (new)
- Managed Publisher Prefix used for unmanaged solution
- Reference to DateTime.Now
- Unfiltered service endpoint step registration
- Invalid .NET Framework version
- Source control alignment
- Component specific Best Practice Violations
- Unfiltered plugin step registration
- Plugin is not registered
- Target Update
- Missing plugin base class
- Stateful Plugins
- Parallel SDK Call
- Resource intensive API
- Batch request
- Recursive plugin execution
- Default Plugin Assembly Version
- Step registration for Retrieve and RetrieveMultiple
- Source control alignment
- Static entity reference in cloud flow
- Static entity reference in workflow
- Static Guid usage
- Duplicated plugin step registration
- KeepAlive when interacting with external calls
- InvalidPluginExecutionException in plugin
- ITracingService in plugin
- Timeout when making external calls
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:
High
Medium
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
Go to Settings > Customizations.
Select Publishers.
If there is more than one publisher, open the one with the Display Name that starts with Default Publisher for <your organization name>.
At the bottom of the form, update the Prefix field to change the default value of “new” to something that identifies your organization.
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 |
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. |
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.
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