" MicromOne: Building a Custom API in Dynamics 365 with Entity Input and Output Parameters

Pagine

Building a Custom API in Dynamics 365 with Entity Input and Output Parameters

The Scenario

We’ll create a Custom API ValidateExampleRecord that:

  1. Accepts an Entity as input (ValidateExampleRecord_InputEntity),

  2. Performs server-side validation and duplicate checking,

  3. Returns an Entity as output (ValidateExampleRecord_OutputEntity),

  4. 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

  1. Go to Solutions → Open or create a new solution.

  2. Click New → Automation → Custom API.

  3. Fill in the main fields as follows:



Unique NameValidateExampleRecord
Display NameValidate Example Record
Binding TypeNone (Global)
Can be called fromBoth (client and server)
Enabled for WorkflowNo
Execute Privilege Name(leave empty)
DescriptionValidates entity data and checks for duplicates.
  1. Save and publish.




Define Custom API Parameters

Next, add input and output parameters.

Input Parameter



NameValidateExampleRecord_InputEntity
Display NameInput Entity
TypeEntity
Entity Logical Nameexample_entity
Is OptionalNo
DirectionInput

Output Parameter



NameValidateExampleRecord_OutputEntity
Display NameOutput Entity
TypeEntity
Entity Logical Nameexample_entity
DirectionOutput

Then save and publish again.



Custom API Plugin (Server-Side Validation)


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)serviceProvider.GetService(typeof(ITracingService));
            var context =
(IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            var serviceFactory =
(IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            var service = serviceFactory.CreateOrganizationService(null);

            tracing.Trace("START ValidateExampleRecord");

            try
            {
                if
(!context.InputParameters.Contains("ValidateExampleRecord_InputEntity")
||

context.InputParameters["ValidateExampleRecord_InputEntity"] == null)
                    throw new InvalidPluginExecutionException("The
input parameter 'InputEntity' is missing or null.");

                var inputEntity =
(Entity)context.InputParameters["ValidateExampleRecord_InputEntity"];
                tracing.Trace($"Entity: {inputEntity.LogicalName}, ID:
{inputEntity.Id}");

                // Extract attributes
                var recordName =
inputEntity.GetAttributeValue<string>("example_name");
                var recordCode =
inputEntity.GetAttributeValue<string>("example_identifier");
                var parentRefId =
inputEntity.GetAttributeValue<EntityReference>("example_parentreference")?.Id;

                // Validate required attributes
                var missing = new List<string>();
                if (string.IsNullOrWhiteSpace(recordName))
missing.Add("example_name");
                if (string.IsNullOrWhiteSpace(recordCode))
missing.Add("example_identifier");
                if (parentRefId == null || parentRefId == Guid.Empty)
missing.Add("example_parentreference");

                if (missing.Count > 0)
                    throw new
InvalidPluginExecutionException($"Missing required attributes:
{string.Join(", ", missing)}");

                // Run duplicate search
                var existingRecord = FindExisting(service,
inputEntity.Id, recordName, recordCode, parentRefId.Value);

context.OutputParameters["ValidateExampleRecord_OutputEntity"] =
existingRecord;

                tracing.Trace("END ValidateExampleRecord");
            }
            catch (Exception ex)
            {
                tracing.Trace($"Error: {ex}");
                throw new InvalidPluginExecutionException($"Custom API
error: {ex.Message}", ex);
            }
        }

        private Entity FindExisting(IOrganizationService service,
Guid? entityId, string name, string code, Guid parentId)
        {
            tracing.Trace("Executing duplicate search...");
            Entity result = null;

            var query = new QueryExpression("example_entity")
            {
                ColumnSet = new ColumnSet("example_name",
"example_identifier", "example_parentreference")
            };

            query.Criteria.AddCondition("example_name",
ConditionOperator.Equal, name);
            query.Criteria.AddCondition("example_parentreference",
ConditionOperator.Equal, parentId);
            if (entityId != null && entityId != Guid.Empty)
                query.Criteria.AddCondition("example_entityid",
ConditionOperator.NotEqual, entityId);

            var matches = service.RetrieveMultiple(query).Entities;
            tracing.Trace($"Found {matches.Count} possible duplicates.");

            var existing = matches.FirstOrDefault();
            if (existing != null)
            {
                var existingCode =
existing.GetAttributeValue<string>("example_identifier");
                if (!string.IsNullOrWhiteSpace(existingCode) &&
                    string.Equals(existingCode.Trim(), code.Trim(),
StringComparison.OrdinalIgnoreCase))
                {
                    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.getFormContext();

        if (!ALLOW_SAVE) {
            APP.Record.CheckRecord(formContext, function () {
                var dialogOptions = {
                    title: "Duplicate Detected",
                    text: "A record with similar values already
exists. Do you want to continue?",
                    confirmButtonLabel: "Confirm",
                    cancelButtonLabel: "Cancel"
                };


Xrm.Navigation.openConfirmDialog(dialogOptions).then(function
(response) {
                    if (response.confirmed) {
                        ALLOW_SAVE = true;
                        formContext.data.save();
                    } else {
                        ExecutionContext.getEventArgs().preventDefault();
                    }
                });
            });
        }

        ALLOW_SAVE = false;
    },

    CheckRecord: function (formContext, callback) {
        var name = formContext.getAttribute("example_name")?.getValue() || "";
        var code =
formContext.getAttribute("example_identifier")?.getValue() || "";
        var parent =
formContext.getAttribute("example_parentreference")?.getValue();
        var recordId =
formContext.data.entity.getId()?.replace(/[{}]/g, "") ||
"00000000-0000-0000-0000-000000000000";

        var inputEntity = {
            "@odata.type": "Microsoft.Dynamics.CRM.example_entity",
            "example_entityid": recordId,
            "example_name": name,
            "example_identifier": code
        };

        if (parent && parent.length > 0) {
            inputEntity["example_parentreference@odata.bind"] =
`/example_parents(${parent[0].id.replace(/[{}]/g, "")})`;
        }

        var request = {
            ValidateExampleRecord_InputEntity: inputEntity,
            getMetadata: function () {
                return {
                    boundParameter: null,
                    parameterTypes: {
                        ValidateExampleRecord_InputEntity: { typeName:
"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-type");
                return type && type.indexOf("application/json") !== -1
? 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)serviceProvider.GetService(typeof(ITracingService));
            var context =
(IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            var serviceFactory =
(IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            var service =
serviceFactory.CreateOrganizationService(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.Contains("Target") ||
!(context.InputParameters["Target"] is Entity target))
                throw new InvalidPluginExecutionException("Target
entity is missing or invalid.");

            // Get PreImage for Update
            Entity preImage = null;
            if (context.MessageName == "Update" &&
context.PreEntityImages.Contains("PreImage"))
                preImage = context.PreEntityImages["PreImage"];

            // Merge data between target and preImage
            string recordName = target.GetAttributeValue<string>("example_name")
                                ??
preImage?.GetAttributeValue<string>("example_name");

            string recordCode =
target.GetAttributeValue<string>("example_identifier")
                                ??
preImage?.GetAttributeValue<string>("example_identifier");

            EntityReference parentRef =
target.GetAttributeValue<EntityReference>("example_parentreference")
                                        ??
preImage?.GetAttributeValue<EntityReference>("example_parentreference");

            // 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(recordName))
                entityToValidate["example_name"] = recordName;

            if (!string.IsNullOrWhiteSpace(recordCode))
                entityToValidate["example_identifier"] = recordCode;

            if (parentRef != null)
                entityToValidate["example_parentreference"] = parentRef;

            tracing.Trace("Calling Custom API: ValidateExampleRecord");

            // Execute Custom API
            var request = new OrganizationRequest("ValidateExampleRecord")
            {
                ["ValidateExampleRecord_InputEntity"] = entityToValidate
            };

            var response = service.Execute(request);
            var duplicate =
response.Results["ValidateExampleRecord_OutputEntity"] as Entity;

            // Handle duplicate scenario
            if (duplicate != null && duplicate.Id != Guid.Empty)
                throw new InvalidPluginExecutionException(
                    "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