The Scenario
We’ll create a Custom API ValidateExampleRecord that:
-
Accepts an Entity as input (
ValidateExampleRecord_InputEntity), -
Performs server-side validation and duplicate checking,
-
Returns an Entity as output (
ValidateExampleRecord_OutputEntity), -
Can be called from:
-
JavaScript (e.g., form OnSave),
-
Another plugin (PreOperation stage).
Create the Custom API in Dataverse
You can create it either via Power Apps Maker Portal or Power Platform CLI (PAC).
Using Power Apps Maker Portal
-
Go to Solutions → Open or create a new solution.
-
Click New → Automation → Custom API.
-
Fill in the main fields as follows:
| Unique Name | ValidateExampleRecord |
| Display Name | Validate Example Record |
| Binding Type | None (Global) |
| Can be called from | Both (client and server) |
| Enabled for Workflow | No |
| Execute Privilege Name | (leave empty) |
| Description | Validates entity data and checks for duplicates. |
-
Save and publish.
Define Custom API Parameters
Next, add input and output parameters.
Input Parameter
| Name | ValidateExampleRecord_InputEntity |
| Display Name | Input Entity |
| Type | Entity |
| Entity Logical Name | example_entity |
| Is Optional | No |
| Direction | Input |
Output Parameter
| Name | ValidateExampleRecord_OutputEntity |
| Display Name | Output Entity |
| Type | Entity |
| Entity Logical Name | example_entity |
| Direction | Output |
Then save and publish again.
Here’s a sample C# plugin class that implements the logic behind our Custom API.
It accepts an entity as input, validates its content, and outputs a
matching entity if found.
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Example.CustomAPIs
{
public class ValidateExampleRecord : IPlugin
{
ITracingService tracing;
public void Execute(IServiceProvider serviceProvider)
{
tracing =
(ITracingService)serviceProvid
var context =
(IPluginExecutionContext)servi
var serviceFactory =
(IOrganizationServiceFactory)s
var service = serviceFactory.CreateOrganizat
tracing.Trace("START ValidateExampleRecord");
try
{
if
(!context.InputParameters.Cont
||
context.InputParameters["Valid
throw new InvalidPluginExecutionExceptio
input parameter 'InputEntity' is missing or null.");
var inputEntity =
(Entity)context.InputParameter
tracing.Trace($"Entity: {inputEntity.LogicalName}, ID:
{inputEntity.Id}");
// Extract attributes
var recordName =
inputEntity.GetAttributeValue<
var recordCode =
inputEntity.GetAttributeValue<
var parentRefId =
inputEntity.GetAttributeValue<
// Validate required attributes
var missing = new List<string>();
if (string.IsNullOrWhiteSpace(rec
missing.Add("example_name");
if (string.IsNullOrWhiteSpace(rec
missing.Add("example_identifie
if (parentRefId == null || parentRefId == Guid.Empty)
missing.Add("example_parentref
if (missing.Count > 0)
throw new
InvalidPluginExecutionExceptio
{string.Join(", ", missing)}");
// Run duplicate search
var existingRecord = FindExisting(service,
inputEntity.Id, recordName, recordCode, parentRefId.Value);
context.OutputParameters["Vali
existingRecord;
tracing.Trace("END ValidateExampleRecord");
}
catch (Exception ex)
{
tracing.Trace($"Error: {ex}");
throw new InvalidPluginExecutionExceptio
error: {ex.Message}", ex);
}
}
private Entity FindExisting(IOrganizationServ
Guid? entityId, string name, string code, Guid parentId)
{
tracing.Trace("Executing duplicate search...");
Entity result = null;
var query = new QueryExpression("example_entit
{
ColumnSet = new ColumnSet("example_name",
"example_identifier", "example_parentreference")
};
query.Criteria.AddCondition("e
ConditionOperator.Equal, name);
query.Criteria.AddCondition("e
ConditionOperator.Equal, parentId);
if (entityId != null && entityId != Guid.Empty)
query.Criteria.AddCondition("e
ConditionOperator.NotEqual, entityId);
var matches = service.RetrieveMultiple(query
tracing.Trace($"Found {matches.Count} possible duplicates.");
var existing = matches.FirstOrDefault();
if (existing != null)
{
var existingCode =
existing.GetAttributeValue<str
if (!string.IsNullOrWhiteSpace(ex
string.Equals(existingCode.Tri
StringComparison.OrdinalIgnore
{
result = existing;
tracing.Trace("Duplicate record found.");
}
}
return result;
}
}
}
Calling the Custom API from JavaScript
You can trigger this Custom API from the form’s OnSave event to
prevent users from creating duplicates.
This example uses Xrm.WebApi.execute() to send the entity data to the
Custom API.
var formContext;
var ALLOW_SAVE = false;
if (typeof (APP) == "undefined") {
APP = {};
}
APP.Record = {
OnSave: function (ExecutionContext) {
formContext = ExecutionContext.getFormContex
if (!ALLOW_SAVE) {
APP.Record.CheckRecord(formCon
var dialogOptions = {
title: "Duplicate Detected",
text: "A record with similar values already
exists. Do you want to continue?",
confirmButtonLabel: "Confirm",
cancelButtonLabel: "Cancel"
};
Xrm.Navigation.openConfirmDial
(response) {
if (response.confirmed) {
ALLOW_SAVE = true;
formContext.data.save();
} else {
ExecutionContext.getEventArgs(
}
});
});
}
ALLOW_SAVE = false;
},
CheckRecord: function (formContext, callback) {
var name = formContext.getAttribute("exam
var code =
formContext.getAttribute("exam
var parent =
formContext.getAttribute("exam
var recordId =
formContext.data.entity.getId(
"00000000-0000-0000-0000-00000
var inputEntity = {
"@odata.type": "Microsoft.Dynamics.CRM.exampl
"example_entityid": recordId,
"example_name": name,
"example_identifier": code
};
if (parent && parent.length > 0) {
inputEntity["example_parentref
`/example_parents(${parent[0].
}
var request = {
ValidateExampleRecord_InputEnt
getMetadata: function () {
return {
boundParameter: null,
parameterTypes: {
ValidateExampleRecord_InputEnt
"mscrm.example_entity", structuralProperty: 5 }
},
operationType: 0,
operationName: "ValidateExampleRecord"
};
}
};
Xrm.WebApi.execute(request)
.then(async response => {
if (!response.ok) throw new Error("HTTP Error: " +
response.status);
const type = response.headers.get("content-
return type && type.indexOf("application/json
? await response.json() : null;
})
.then(result => {
if (result) callback();
})
.catch(error => console.error("Error in CheckRecord:", error));
}
};
Calling the Custom API from Another Plugin (with Target + PreImage)
In this version, the plugin executes during the Pre-Operation stage
and uses both the incoming Target entity (the data being updated or
created) and a PreImage (the record as it was before the update).
This ensures consistent behavior whether the operation is a Create or Update.
using Microsoft.Xrm.Sdk;
using System;
namespace Example.Plugins
{
public class ExampleRecordPreOperation : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var tracing =
(ITracingService)serviceProvid er.GetService(typeof( ITracingService));
var context =
(IPluginExecutionContext)servi ceProvider.GetService(typeof(I PluginExecutionContext));
var serviceFactory =
(IOrganizationServiceFactory)s erviceProvider.GetService(type of(IOrganizationServiceFactory ));
var service =
serviceFactory.CreateOrganizat ionService(context. InitiatingUserId);
tracing.Trace("START - ExampleRecordPreOperation");
// Ensure plugin registration context
if (context.Stage != 20 || (context.MessageName !=
"Create" && context.MessageName != "Update"))
{
tracing.Trace($"Unexpected context:
Stage={context.Stage}, Message={context.MessageName}" );
return;
}
// Get target entity
if (!context.InputParameters.Cont ains("Target") ||
!(context.InputParameters["Tar get"] is Entity target))
throw new InvalidPluginExecutionExceptio n("Target
entity is missing or invalid.");
// Get PreImage for Update
Entity preImage = null;
if (context.MessageName == "Update" &&
context.PreEntityImages.Contai ns("PreImage"))
preImage = context.PreEntityImages["PreIm age"];
// Merge data between target and preImage
string recordName = target.GetAttributeValue<strin g>("example_name")
??
preImage?.GetAttributeValue<st ring>("example_name");
string recordCode =
target.GetAttributeValue<strin g>("example_identifier")
??
preImage?.GetAttributeValue<st ring>("example_identifier");
EntityReference parentRef =
target.GetAttributeValue<Entit yReference>("example_parentref erence")
??
preImage?.GetAttributeValue<En tityReference>("example_parent reference");
// Prepare the entity for Custom API validation
var entityToValidate = new Entity("example_entity");
if (context.MessageName == "Update" && target.Id != Guid.Empty)
entityToValidate.Id = target.Id;
if (!string.IsNullOrWhiteSpace(re cordName))
entityToValidate["example_name "] = recordName;
if (!string.IsNullOrWhiteSpace(re cordCode))
entityToValidate["example_iden tifier"] = recordCode;
if (parentRef != null)
entityToValidate["example_pare ntreference"] = parentRef;
tracing.Trace("Calling Custom API: ValidateExampleRecord");
// Execute Custom API
var request = new OrganizationRequest("ValidateE xampleRecord")
{
["ValidateExampleRecord_InputE ntity"] = entityToValidate
};
var response = service.Execute(request);
var duplicate =
response.Results["ValidateExam pleRecord_OutputEntity"] as Entity;
// Handle duplicate scenario
if (duplicate != null && duplicate.Id != Guid.Empty)
throw new InvalidPluginExecutionExceptio n(
"A record with the same name and identifier
already exists for this parent record.");
tracing.Trace("END - ExampleRecordPreOperation");
}
}
}
Optional: Registering the Plugin
In your plugin registration tool (e.g., Plugin Registration Tool or
Power Platform CLI):
Stage: Pre-Operation (20)
Messages: Create and Update
Primary Entity: example_entity
PreImage Name: PreImage
PreImage Attributes: example_name, example_identifier, example_parentreference
In this version, the plugin executes during the Pre-Operation stage
and uses both the incoming Target entity (the data being updated or
created) and a PreImage (the record as it was before the update).
This ensures consistent behavior whether the operation is a Create or Update.
using Microsoft.Xrm.Sdk;
using System;
namespace Example.Plugins
{
public class ExampleRecordPreOperation : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var tracing =
(ITracingService)serviceProvid
var context =
(IPluginExecutionContext)servi
var serviceFactory =
(IOrganizationServiceFactory)s
var service =
serviceFactory.CreateOrganizat
tracing.Trace("START - ExampleRecordPreOperation");
// Ensure plugin registration context
if (context.Stage != 20 || (context.MessageName !=
"Create" && context.MessageName != "Update"))
{
tracing.Trace($"Unexpected context:
Stage={context.Stage}, Message={context.MessageName}"
return;
}
// Get target entity
if (!context.InputParameters.Cont
!(context.InputParameters["Tar
throw new InvalidPluginExecutionExceptio
entity is missing or invalid.");
// Get PreImage for Update
Entity preImage = null;
if (context.MessageName == "Update" &&
context.PreEntityImages.Contai
preImage = context.PreEntityImages["PreIm
// Merge data between target and preImage
string recordName = target.GetAttributeValue<strin
??
preImage?.GetAttributeValue<st
string recordCode =
target.GetAttributeValue<strin
??
preImage?.GetAttributeValue<st
EntityReference parentRef =
target.GetAttributeValue<Entit
??
preImage?.GetAttributeValue<En
// Prepare the entity for Custom API validation
var entityToValidate = new Entity("example_entity");
if (context.MessageName == "Update" && target.Id != Guid.Empty)
entityToValidate.Id = target.Id;
if (!string.IsNullOrWhiteSpace(re
entityToValidate["example_name
if (!string.IsNullOrWhiteSpace(re
entityToValidate["example_iden
if (parentRef != null)
entityToValidate["example_pare
tracing.Trace("Calling Custom API: ValidateExampleRecord");
// Execute Custom API
var request = new OrganizationRequest("ValidateE
{
["ValidateExampleRecord_InputE
};
var response = service.Execute(request);
var duplicate =
response.Results["ValidateExam
// Handle duplicate scenario
if (duplicate != null && duplicate.Id != Guid.Empty)
throw new InvalidPluginExecutionExceptio
"A record with the same name and identifier
already exists for this parent record.");
tracing.Trace("END - ExampleRecordPreOperation");
}
}
}
Optional: Registering the Plugin
In your plugin registration tool (e.g., Plugin Registration Tool or
Power Platform CLI):
Stage: Pre-Operation (20)
Messages: Create and Update
Primary Entity: example_entity
PreImage Name: PreImage
PreImage Attributes: example_name, example_identifier, example_parentreference