Pre-operation plugin for duplicate checks
Block duplicate contacts before they are saved, with a small C# plugin registered on the pre-operation stage — and the mistakes to avoid.
What you'll learn
- Pick the right pipeline stage for validation
- Query Dataverse safely inside a plugin
- Register Create and Update steps with filtering attributes
Duplicate detection rules in Dynamics 365 are great for warning users, but they can be skipped. When a duplicate must never reach the database — for example two contacts with the same email — a plugin is the reliable answer.
The problem
Users create the same contact twice from different places: a form, an import, a Power Automate flow. Duplicate detection only warns in some of those paths, so bad data slips in.
Which stage to register on
A plugin step can run in three synchronous stages:
| Stage | Number | Runs inside the transaction? |
|---|---|---|
| Pre-validation | 10 | No |
| Pre-operation | 20 | Yes |
| Post-operation | 40 | Yes |
For this check we use pre-operation (20). The record is not saved yet, so throwing an exception stops the save cleanly.
The plugin code
using System;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
public class ValidateDuplicateContact : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
var factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
var service = factory.CreateOrganizationService(context.UserId);
var tracing = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
if (!(context.InputParameters["Target"] is Entity target)) return;
var email = target.GetAttributeValue<string>("emailaddress1");
if (string.IsNullOrWhiteSpace(email)) return;
var query = new QueryExpression("contact")
{
ColumnSet = new ColumnSet(false),
TopCount = 1
};
query.Criteria.AddCondition("emailaddress1", ConditionOperator.Equal, email.Trim());
query.Criteria.AddCondition("statecode", ConditionOperator.Equal, 0);
if (service.RetrieveMultiple(query).Entities.Count > 0)
{
tracing.Trace("Duplicate found for {0}", email);
throw new InvalidPluginExecutionException(
$"A contact with the email {email} already exists.");
}
}
}Registering the step
- Build the assembly and register it with the Plugin Registration Tool.
- Add a step: Message
Create, Primary entitycontact, StagePreOperation, ModeSynchronous. - Add a second step on
Updatewith filtering attributes set toemailaddress1, so it only fires when the email changes.
Testing
- Create a contact with a new email — it saves.
- Create another with the same email — you get the friendly error.
- Check Plug-in Trace Log to see the
Tracemessage when tracing is enabled.
That's it: a small plugin, one query, and your data stays clean no matter where it comes from.