Prompt & Platform
Connect
HomeTopicsSnippetsDev toolsArticles
Theme
Pro-codeIntermediate

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.

Prerequisites

  • Visual Studio with the Dataverse SDK
  • Plugin Registration Tool
  • A Dataverse dev environment

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
On this page
  1. The problem
  2. Which stage to register on
  3. The plugin code
  4. Registering the step
  5. Testing

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:

StageNumberRuns inside the transaction?
Pre-validation10No
Pre-operation20Yes
Post-operation40Yes

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

C#
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

  1. Build the assembly and register it with the Plugin Registration Tool.
  2. Add a step: Message Create, Primary entity contact, Stage PreOperation, Mode Synchronous.
  3. Add a second step on Update with filtering attributes set to emailaddress1, 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 Trace message when tracing is enabled.

That's it: a small plugin, one query, and your data stays clean no matter where it comes from.

J / K next or previous article

Jagadeesan P

Found this useful? I share new guides on LinkedIn.

Follow