Plug-Ins
Create Plug-In Project
When creating a new plug-in project, you have two options to choose from on how to begin:
- Use Power Platform CLI to create the project for you (uses preview features at time of writing)
- Create plug-in project manually from Visual Studio
Always follow the code structure guidelines when setting up your solutions.
Using Power Platform CLI
When using Power Platform CLI, it is recommended to create a new Visual Studio solution in advance.
To create a new project:
- Install Power Platform CLI.
- Navigate to the
src/{SolutionName}
folder in a command line window. - Use
mkdir {SolutionName}.Dataverse.Plugins
command to create plug-in folder. - Navigate to the new folder with
cd {SolutionName}.Dataverse.Plugins
. - Create the new project with
pac plugin init
command. - Add the project to your existing Visual Studio solution.
When creating a solution this way, the project will already be making use of the new Dependent Assembly plug-ins (preview) feature, which makes it possible to include other assemblies in your project, such as NuGet packages like Newtonsoft.Json
.
Using Visual Studio
A plug-in solution can be created manually with Visual Studio as well, however in this case we will be using ILMerge to make it possible to include other assemblies with our project. Note that ILMerge is not officially supported by Microsoft and might cause deployment issues when merging with too large assemblies.
To create a new project:
- Open the existing solution in Visual Studio, or create a new one.
- Select File/New/Project option.
- Select Class Library and select Next. Important: Do not select Class Library (.NET Framework). *
- Create the project with the name
{SolutionName}.Dataverse.Plugins
inside thesrc/{SolutionName}/
folder. - Right click on the project in the Solution Explorer and select Properties.
- Under Build/Strong naming select the Sign the assembly option and provide a Strong name key file. You can create an snk file using the
Developer Command Prompt for VS
.
* We are choosing the .NET Core based Class Library in favor of the legacy .NET Framework as it comes with the new csproj structure.
As plug-in class libraries only support the legacy .NET Framework, we need to update the content of the csproj
file's TargetFramework
property (everything else can be left as is):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net462</TargetFramework>
</PropertyGroup>
</Project>
Install the following NuGet packages to begin plug-in development:
- Microsoft.CrmSdk.CoreAssemblies
- ILMerge.Tools
- ILmerge.MSBuild.Task (by Emerson Brito)
Next Steps
Once the plug-in project is ready - whether it was created using the Power Platform CLI or manually with Visual Studio -, the project must be configured:
- Create a new project in Visual Studio by following the steps in the previous step, naming the project
{SolutionName}.Dataverse.PluginExtensions
. - Install the
OSB.Dataverse.PluginExtensions
NuGet package from our internalhttps://pkgs.dev.azure.com/osbgroup/_packaging/osb-d365-library/nuget/v3/index.json
NuGet library. - Once the classes of the library has been added to the project, you can remove the NuGet package.
- Reference the
PluginExtensions
project from thePlugins
project. - (Optional) Create the early bound entities project and reference it from the
Plugins
project as well. - (Optional when using ILMerge) Create an
ILMergeConfig.json
file to control the behavior of DLL merging. Add the name of all assemblies to theInputAssemblies
array which is referenced from the plugin project:
{
"General": {
"InputAssemblies": [
"$(TargetDir)\\{SolutionName}.Dataverse.PluginExtensions.dll",
"$(TargetDir)\\{SolutionName}.Dataverse.DTO.dll"
],
"OutputFile": "$(TargetDir)ILMerge/$(TargetFileName)"
},
"Advanced": {
"DebugInfo": false
}
}
Code Structure
Code structure of your projects should always follow a specific pattern to make it easier for your fellow developers to find what they are looking for.
All Power Platform code component projects should follow the folder structure as shown below:
- docs/ # Optional: Any documents related to the project
- pipelines/ # Pipeline definitions
- solution/
- ./{SolutionName}/
- ./Unmanaged/ # Folder containing the unpacked unmanaged solution
- ./Managed/ # Folder containing the unpacked managed solution
- src/
- ./{SolutionName}/
- ./{SolutionName}.Dataverse.DTO/ # Early bound entities project
- ./{SolutionName}.Dataverse.Plugins/ # Plugins projects
- ./{SolutionName}.Dataverse.PluginExtensions/ # PluginBase project
- ./{SolutionName}.sln
- xrm-connections/
- ./{project}.xrm-connections.xml # XrmToolbox connections file
- README.md
Create a plug-in
All plug-ins must be added to the Plugins
project of the solution. Plug-ins should be organized by the table which they are associated with. Each table with a plug-in should have its own folder created, named after its schema name.
An example folder structure for the Plugins
project:
- account/
- myproj_customtable/
- myproj_othertable/
- {SolutionName}.Dataverse.Plugins.csproj
- ILMergeConfig.json
All plug-ins must be derived from the PluginBase
class (which is coming from the PluginExtensions
project). The PluginBase
class implements a method for each message that can be overridden to implement custom logic for the different messages.
Naming of the plug-in should follow any of the following rules:
- Business logic summary: The name should be a short summary of the business logic within the file. E.g.:
AddRelevantPartiesOnCreate.cs
. - Event Handler: The name reflects to an event triggering the plug-in. E.g.:
HandleOwnerChange.cs
. - Registration definition: The name reflects to the registration definition of the plug-in. E.g.:
PostMyTableUpdateAsync
.
A plug-in file should have a format as follows after it has been created:
public class AddRelevantPartiesOnCreate : PluginBase
{
public override void HandlePostCreateMessage(PluginExecutionContext context)
{
// Add plug-in logic here
}
}
Early Bound Entities
XrmToolBox has many different tools, including the Early Bound Generator by Daryl LaBar helping us developers a lot.
When generating your early bound entities for a project, always make sure that you:
- Generate early bound entities to a separate project, e.g.:
YourProject.Dataverse.DTO
. - Save the settings XML file into the project folder, with a name referring to the project, e.g.:
YourProject.EarlyBoundGenerator.Settings.xml
. - Commit updates made to the early bound entities/settings file in a separate commit to make these changes easier to track.
- Always generate the early bound entities from the development environment.
There are some settings you need to make sure you configure properly when generating the early bound entities:
- Entities
- Entities Prefix Whitelist: Specify the prefix of the solution to include all tables that has been created for the soluiton.
- Entities Whitelist: If for any reason you would need to include a table outside of the solution, add it to the whitelist.
- Generate Entity Attribute Name Constants: Always set to
true
. This is important for making updates and changes for records using theIOrganizationService
.
Updating early bound records
While working with early bound entities has its own benefits, there are some drawbacks as well. One of them is that you should not use the early bound classes to create or update a record.
Instead, you should always instantiate a new Entity
object and assign field values usng the Attribute Name Constants:
var account = organizationService.Retrieve("account", "a709d67d-5603-4daf-a8da-4c7046ba4421").ToEntity<Account>();
// Incorrect way
account.emailaddress1 = "john.doe@contoso.com";
organizationService.Update(account);
// Correct way
var updateRequest = new Entity(account.EntityLogicalName, account.Id);
updateRequest[Account.Fields.emailaddress1] = "john.doe@contoso.com";
organizationService.Update(updateRequest);
Note: When updating an existing record it is always important to use a new instance of Entity
class including only the attributes that has changed. Otherwise our update might trigger unwanted plug-in/workflow executions or any other business logic.
Logging
Easier debugging with frequent tracing
To make debugging and tracing errors easier, it is recommended to add a Trace
call at least at every if-condition junctions in your call.
Example:
if (!string.IsNullOrWhitespace(account.emailaddress1))
{
context.Trace("Email Address contains data. Beginning validation.");
var isValid = emailValidator.Validate(account.emailaddress1);
if (isValid)
context.Trace("Email Address is valid.");
else
context.Trace("Email Address is invalid.");
}
else
context.Trace("Email Address does not contain data. Skipping execution.");
When an error occurs, it will become easier to define the exact location of the error when using such methods for logging.
Include the method name
To improve maintainability of your code, it is recommended to organize and structure your code into separate classes and methods. However, this might make it more difficult to track the source of a trace message.
To overcome this issue, you can either include the method name in each of your calls or use the following example method to always include it for you:
public void DoSomeLogic()
{
// Option 1
conext.Trace($"[{nameof(DoSomeLogic)}] - Beginning code execution...");
// Output: [DoSomeLogic] - Beginning code execution...
// Option 2
TraceMessage("Beginning code execution...");
// Output: [DoSomeLogic] - Beginning code execution...
}
/// <summary>
/// Trace a message with the caller method included.
/// </summary>
public void TraceMessage(string message, [CallerMemberName] string caller = null)
{
if (caller != null) {
context.Trace($"[{caller}] - {message}");
} else {
context.Trace(message);
}
}