With Plug-ins we can extend or customize the functionality of the Microsoft Dynamics CRM platform by integrating the custom business logic (code). Plug-ins are triggered by the message with which they're registered on the Microsoft Dynamics CRM platform.
For example, we can register a Plug-in to perform some business logic every time a Flight record is created. We can also define whether to run the business logic BEFORE the Flight record is saved in the CRM organization database (Pre-Event) or AFTER the Flight record is saved in the CRM organization database (Post-Event).
Notice that some of the business logic can also be accomplished with JavaScript, which is a Client-Side programming method, such as data validation or user interface design, and so on. The Client-Side script depends on the user's browser and is triggered by the CRM form events. By contrast, the plug-ins are triggered by the platform events—that is, importing bulk Flight records can trigger Plug-in events, but doesn't trigger the form events.
CRM 2011 allows Plug-ins to participate in SQL transactions, and allows them to create traces returned with exceptions. In the previous version of CRM Online, we cannot deploy Plug-ins and custom workflow activities to the environment. The current version of CRM 2011 supports the execution of Plug-ins in an isolated environment. In this isolated environment, also known as a sandbox, a Plug-in can make use of the full power of the CRM SDK to access the Web Services, in order to perform custom business logic. However, placing Plug-ins in a sandbox prevents you from accessing the file systems, system event log, registries, and network resources. However, sandbox Plug-ins do have access to the external endpoints like the Windows Azure cloud.
Microsoft Dynamics CRM 2011 and Microsoft Dynamics CRM Online provide the ability to add custom business logic to the event pipeline on the Microsoft Dynamics CRM server. We call this the event framework. The event framework allows developers to create rich solutions on top of Microsoft CRM, and provides the following key features:
The Plug-ins execute in the Event Framework based on a message pipeline execution model. This can be registered in either synchronous mode or asynchronous mode. The CRM platform core operation and any Plug-ins registered for synchronous execution are executed immediately (executed in a well-defined order). Plug-ins registered for asynchronous execution are queued with the Asynchronous Service in Microsoft Dynamics CRM, and executed at a later time. The following diagram shows the event execution pipeline for Plug-ins:
On the code level, a Plug-in is a custom class that implements the IPlugin interface. A Plug-in can be written in any .NET Framework 4 CLR-compliant language, such as C# and VB .NET in Microsoft Visual Studio 2010. Typical Plug-ins access the information in the context, perform the required business operations, and handle exceptions.
Let's take a look at the SDK example of the Plug-in code structure in Visual Studio:
using System; usingSystem.ServiceModel; usingSystem.Runtime.Serialization; usingMicrosoft.Xrm.Sdk; namespacePluginsSample { public class Class1:IPlugin { public void Execute(IServiceProviderserviceProvider) { // Obtain the execution context from the service provider. IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); // Get a reference to the organization service. IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); IOrganizationService service = factory.CreateOrganizationService(context.UserId); // Get a reference to the tracing service. ITracingServicetracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); try { // Plug-in business logic goes below this line. // Invoke organization service methods. } catch (FaultException<OrganizationServiceFault> ex) { // Handle the exception. } } } }
Add Microsoft.Xrm.Sdk.dll
and Microsoft.Crm.Sdk.Proxy.dll
assembly references to the CRM project, in order to access the CRM context, and then compile the Plug-in code. In addition to these two, you can also reference the following out-of-the-box CRM server DLLs for different purposes:
The IPlugin is the base interface for all Plug-ins. The Execute method is also a required method for all Plug-ins. The IServiceProvider parameter of the Execute method is a container for several objects that can be accessed within a Plug-in. The serviceProvider
is an instance of the IServiceProvider
, which contains references to the execution context (IPluginExecutionContext), IOrganizationServiceFactory, ITracingService, and so on.
The InputParameters property contains the data that is in the request message that triggered the event that caused the Plug-in to execute.
The OutputParameters property contains the data that is in the response message, after the core platform operation has completed.
// The InputParameters collection contains all the data passed// in the message request. if (context.InputParameters.Contains("Target") &&context.InputParameters["Target"] is Entity) { // Obtain the target entity from the input parmameters. Entity entity = (Entity)context.InputParameters["Target"];
PreEntityImages contain the primary entity's attributes (that are set to a value or null) before the core platform operation begins.
PostEntityImages contain the primary entity's attributes (that are set to a value or null) after the core platform operation.
Each Plug-in assembly must be signed either by Visual Studio or by the Strong Name tool. As a best practice, do not develop Plug-in code that contains any system logon information, confidential information, or company trade secrets.
Now that we have covered the essential knowledge of the Microsoft Dynamics CRM 2011 Server-Side programming. There is a lot of detailed information in the CRM SDK; please refer to it for a more comprehensive understanding.
It's time to start building a Plug-in example for the ACM system; it will be a Sandbox mode assembly, because we are using Microsoft Dynamics CRM 2011 Online.
The requirement is simple; we want to create a compensation record for each Flight Crew member who served on the flight. Because Flight and Flight Crews are connected through the "Connection" entity (by doing that you can set roles to individuals), we can say: create a compensation record for each connection record created for Flight and Flight Crews with the correct connection roles. The following diagram describes the structure of Connection Roles:
First of all, create a new Connection Role Category. Go to ACM Solutions | Option Sets | Add Existing, and then select Category (connectionrole_category
). Next, go to ACM Solutions | Option Sets, double-click on Category, and then add a new option called "Flight Crew".
Next, create several Connection Roles. Go to the ACM Solutions | Connection Roles, and click the New button to create the following roles in the "Flight Crew" category that we have created.
Name |
Record Type |
Matching Roles |
Connection Role Category |
---|---|---|---|
Cockpit Crew |
Flight |
Captain; First Officer |
Flight Crew |
Cabin Crew |
Flight |
Purser; Flight Attendant | |
Captain |
Flight Crew |
Cockpit Crew | |
First Officer |
Flight Crew |
Cockpit Crew | |
Purser |
Flight Crew |
Cabin Crew | |
Flight Attendant |
Flight Crew |
Cabin Crew |
The following screenshot shows what it should look like in the end:
A Flight can have many connections that connect to CrewMembers to different roles. See the following screenshot as an example:
The following screenshot describes the relationships between Flight, Crew, and Compensation:
The following diagram shows the process flow for this plug-in:
Please carry out the following steps to create a CRM 2011 Plug-in and register the Plug-in on the Create, Update, and Delete message of the Connection entity:
CompensationGeneration
, based on the .NET Framework 4, C#.SDKin
folder of the SDK):microsoft.xrm.sdk.dll
microsoft.crm.sdk.proxy.dll
System.Runtime.Serialization
System.ServiceModel
crmsvcutil.exe
command to generate strongly-typed classes for the entities in the ACM organization, for early binding:sdkin>crmsvcutil.exe /url:http://localhost:5555/ACM/XRMServices/2011/Organization.svc /out:GeneratedCode.cs
GeneratedCode.cs
into solution.QueryConnection.cs
. This does the query work in CRM, listing all flights and their crew members. See the following code:using System; usingSystem.Collections.Generic; usingSystem.Linq; usingSystem.Text; usingSystem.ServiceModel; usingMicrosoft.Xrm.Sdk; usingMicrosoft.Xrm.Sdk.Client; namespace CompensationGeneration { public class CompensationGenerationClass: IPlugin { public void Execute(IServiceProviderserviceProvider) { // Obtain the execution context from the service provider. IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); // Extract the tracing service for use in debugging // sandboxed plug-ins. ITracingServicetracingService = (ITracingService)serviceProvider.GetService( typeof(ITracingService)); // Obtain the organization service reference. IOrganizationServiceFactoryserviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); // Use the context service to create an instance of IOrganizationService. IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId); try { // The InputParameters collection contains all the data passed in the message request. if (context.InputParameters.Contains("Target") &&context.InputParameters["Target"] is Entity) { if (context.MessageName == "Create") { // Obtain the target entity from the input parmameters. Entity entity = (Entity)context.InputParameters["Target"]; // Verify that the entity represents a connection. if (entity.LogicalName != "connection") return; //Gets the entity as the connection type. Connection connection = entity.ToEntity<Connection>(); // If the connection record match the connection role,then create a new compensation. if (CheckConnectionRecords(service, connection)) { CreateCompenstaion(service, connection); } } if (context.MessageName == "Update") { // Gets the properties of the primary entity before the core platform operation has begins. if (context.PreEntityImages.Contains("ConnectionImage") && context.PreEntityImages["ConnectionImage"] is Entity) { Entity entity = (Entity)context.PreEntityImages["ConnectionImage"]; if (entity.LogicalName != "connection") return; Connection connection = entity.ToEntity<Connection>(); // If the old connection record match the connection role, then delete the old compensation. if (CheckConnectionRecords(service, connection)) { DeleteCompensation(service, context.PrimaryEntityId); } } // Gets the properties of the primary entity after the core platform operation has been completed. if (context.PostEntityImages.Contains("ConnectionImage") && context.PostEntityImages["ConnectionImage"] is Entity) { Entity entity = (Entity)context.PostEntityImages["ConnectionImage"]; if (entity.LogicalName != "connection") return; Connection connection = entity.ToEntity<Connection>(); // If the new connection record match the connection role, then create a new compensation. if (CheckConnectionRecords(service, connection)) { CreateCompenstaion(service, connection); } } } } if (context.InputParameters.Contains("Target") &&context.InputParameters["Target"] is EntityReference) { if (context.MessageName == "Delete") { // Identifies a record. The EntityReference class replaces the Moniker class from Microsoft Dynamics CRM 4.0. EntityReference entity = (EntityReference)context.InputParameters["Target"]; if (entity.LogicalName != "connection") return; using (varorgContext = new OrganizationServiceContext(service)) { // Get the connection record that being deleted. Connection connection = orgContext.CreateQuery<Connection>().Where(c =>c.Id == entity.Id).First(); // If the connection record match the connection role, then delete the existing compensation. if (CheckConnectionRecords(service, connection)) { DeleteCompensation(service, context.PrimaryEntityId); } } } } } catch (FaultException<OrganizationServiceFault> ex) { throw new InvalidPluginExecutionException("An error occurred in the CompensationGeneration plug-in.", ex); } catch (Exception ex) { tracingService.Trace("CompensationGeneration: {0}", ex.ToString()); throw; } } privateboolCheckConnectionRecords(IOrganizationService service, Connection connection) { // Validate the connection record. if (connection.Record1Id != null && connection.Record1RoleId != null && connection.Record2Id != null && connection.Record2RoleId != null) { // Each connection will create two records on each side. We just need to stick on 1 side. if (connection.Record1Id.LogicalName == "contact" &&CheckConnectionRoleCategory(service, connection.Record1RoleId.Id)&& connection.Record2Id.LogicalName == "acm_flight" &&CheckConnectionRoleCategory(service, connection.Record2RoleId.Id)) { // Create a new compensation. return true; } } return false; } privateboolCheckConnectionRoleCategory(IOrganizationService service, GuidRoleID) { using (varorgContext = new OrganizationServiceContext(service)) { varconnectionrole = from cr in orgContext.CreateQuery<ConnectionRole>()wherecr.ConnectionRoleId == RoleIDwherecr.Category.Value == 100000000 //Category="Flight Crew" selectcr; if (connectionrole.ToList().Count > 0) return true; else return false; } } private void CreateCompenstaion(IOrganizationService service, Connection connection) { using (varorgContext = new OrganizationServiceContext(service)) { //Single query to get the crewmember record var crewmember = (from c in orgContext.CreateQuery<Contact>() wherec.ContactId == connection.Record1Id.Idselect c).Single(); Money HourlyDutyPay = new Money(crewmember.acm_HourlyDutyPay.Value); stringBaseCity = crewmember.Address1_City; stringBaseCountry = crewmember.Address1_Country; //Single query to get the flight record var flight = (from f in orgContext.CreateQuery<acm_flight>() wheref.acm_flightId == connection.Record2Id.Idselect f).Single(); intFlightTime = flight.acm_FlightTime.Value; intLayoverTime = flight.acm_LayoverTime.Value; OptionSetValueFlightType = new OptionSetValue(flight.acm_FlightType.Value); //Single query to get the airport record var airport = (from fr in orgContext.CreateQuery<acm_flightroute>() join a in orgContext.CreateQuery<acm_airport>() on fr.acm_To.Id equals a.acm_airportIdwherefr.acm_flightrouteId == flight.acm_FlightRoute.Id select a).Single(); Money PerDiem = new Money(airport.acm_PerDiem.Value); stringLayoverCity = airport.acm_City; stringLayoverCountry = airport.acm_Country; //Create compensation record Entity compensation = new Entity("acm_compensation"); compensation["acm_crewmember"] = new EntityReference(connection.Record1Id.LogicalName, connection.Record1Id.Id); compensation["acm_flight"] = new EntityReference(connection.Record2Id.LogicalName, connection.Record2Id.Id); if (FlightTime> 0) compensation["acm_flighttime"] = FlightTime; if (LayoverTime> 0) compensation["acm_layovertime"] = LayoverTime; if (HourlyDutyPay != null) compensation["acm_hourlydutypay"] = HourlyDutyPay; if (FlightType != null) compensation["acm_flighttype"] = FlightType; if (PerDiem != null) compensation["acm_perdiem"] = PerDiem; // Calculate total compensation if (FlightTime> 0 &&LayoverTime> 0 &&HourlyDutyPay != null &&FlightType != null &&PerDiem != null) { Money Compensation; decimalOnDutyPay = HourlyDutyPay.Value * FlightTime / 60; decimalLayoverPay = PerDiem.Value * LayoverTime / 60; if (FlightType.Value != 1) { OnDutyPay = OnDutyPay * 2; } if (BaseCity == LayoverCity&&BaseCountry == LayoverCountry) { Compensation = new Money(OnDutyPay); } else { Compensation = new Money(OnDutyPay + LayoverPay); } compensation["acm_compensation"] = Compensation; compensation["subject"] = "Compensation created for: " + crewmember.FullName + " on " + flight.acm_name + " by " + connection.Id.ToString(); } service.Create(compensation); } } private void DeleteCompensation(IOrganizationService service, GuidConnectionID) { using (varorgContext = new OrganizationServiceContext(service)) { var compensations = from c in orgContext.CreateQuery<acm_compensation>()wherec.Subject.Contains(ConnectionID.ToString())select c; foreach (acm_compensation compensation in compensations) { service.Delete(acm_compensation.EntityLogicalName, compensation.Id); } } } } }
sdk oolspluginregistrationinDebugPluginRegistration.exe
)The discovery URLs for the worldwide Microsoft Dynamics CRM Online data centers are:
CompensationGeneration Plug-in Steps | |||
Message |
Create |
Delete |
Update |
Primary Entity |
connection |
connection |
connection |
Filtering Attributes |
All |
All |
record1id, record2id, record1roleid, record2roleid |
Run in User's Context |
Calling User |
Calling User |
Calling User |
Eventing Pipeline |
Post-operation |
Pre-operation |
Post-operation |
Execution Mode |
Synchronous |
Synchronous |
Synchronous |
Deployment |
Server |
Server |
Server |
It's also recommended to use the CRM Developer Toolkit when building a Plug-in. The Developer's Toolkit for Microsoft Dynamics CRM 2011 is a set of Visual Studio integration tools focused on accelerating the development of custom code for Dynamics CRM 2011. The Toolkit supports the end-to-end creation and deployment of CRM Plug-ins, custom workflows, Silverlight applications, and other Web resources, including JavaScript and HTML. Dynamics CRM developers can write all of their custom code from within Visual Studio, using native tools and build processes, and then automatically deploy them to the CRM Server.