Contenido

Cómo evitar registros duplicados en eventos de Dynamics 365 Customer Insights (con validación de formularios)

Hace unos meses me preguntaron si existía alguna manera de limitar las inscripciones a un evento en Dynamics 365 Customer Insights Journeys a una sola por persona. Pensaba que no había ninguna configuración para esto, así que decidí investigar. Después de un poco de investigación encontré esta documentación de Microsoft: Customize form submission validation

Entonces pensé: si hay una forma de validar un formulario antes de enviarlo, podemos evitar registros duplicados. Tras algunas pruebas, desarrollé un Plugin que valida el registro del evento al momento de enviar el formulario y bloquea el envío si ya existe una inscripción para esa persona.

“Vale Martin, pero… ¿qué significa esto exactamente?”

Básicamente: se puede limitar el proceso de registro verificando si la persona ya está inscrita en el evento.

¿CÓMO?

Paso 1: data-form-validation en el formulario de registro

Todo comienza configurando el atributo data-validate-submission="true" en el formulario:

/posts/custom-form-submission-validation/image1.png
Agregue data-validate-submission='true' en la etiqueta del formulario

Sin esto, el plugin no se ejecutará, porque es lo que permite disparar el mensaje "msdynmkt_validateformsubmission".

Ejemplo:

<form aria-label="{{EventName}}" class="marketingForm" data-validate-submission="true"
  data-successmessage="Gracias por enviar el formulario." data-errormessage="Hubo un error, inténtalo de nuevo."
  data-waitlistmessage="Has sido añadido a la lista de espera.">

Paso 2: Crear el Plugin

El plugin deserializa los parámetros del formulario, comprueba con la dirección de correo y el ID del evento si ya existe un registro, y en ese caso bloquea el envío.

Para quienes son más de LowCode/NoCode, en palabras simples:
👉 El plugin lee los datos, revisa si el correo ya está registrado en ese evento, y si es así, impide el registro duplicado.

using System;
using Microsoft.Xrm.Sdk;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Text;
using Microsoft.Xrm.Sdk.Query;

namespace UniqueEmailRegistrations
{

    public class CheckIfEmailRegistered : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            var tracing = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            var factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            var service = factory.CreateOrganizationService(context.UserId);


            var requestString = context.InputParameters.Contains("msdynmkt_formsubmissionrequest")
                ? (string)context.InputParameters["msdynmkt_formsubmissionrequest"]
                : null;

            if (string.IsNullOrWhiteSpace(requestString))
            {
                tracing.Trace("No msdynmkt_formsubmissionrequest found; skipping validation.");
                context.OutputParameters["msdynmkt_validationresponse"] = Serialize(new ValidateFormSubmissionResponse
                {
                    IsValid = true,
                    ValidationOnlyFields = new List<string>()
                });
                return;
            }

            var submission = Deserialize<FormSubmissionRequest>(requestString);

            string GetField(string key)
            {
                if (submission?.Fields != null && submission.Fields.TryGetValue(key, out var value))
                    return value;
                return null;
            }

            var emailRaw = GetField("emailaddress1");
            var eventRaw = GetField("event");

            if (string.IsNullOrWhiteSpace(emailRaw))
            {
                tracing.Trace("Email field is empty or missing; skipping duplicate validation.");
                context.OutputParameters["msdynmkt_validationresponse"] = Serialize(new ValidateFormSubmissionResponse
                {
                    IsValid = true,
                    ValidationOnlyFields = new List<string>()
                });
                return;
            }


            if (!Guid.TryParse(eventRaw, out var eventId) || eventId == Guid.Empty)
            {
                tracing.Trace($"Event field missing or not a GUID: '{eventRaw}'. Skipping duplicate validation.");
                context.OutputParameters["msdynmkt_validationresponse"] = Serialize(new ValidateFormSubmissionResponse
                {
                    IsValid = true,
                    ValidationOnlyFields = new List<string>()
                });
                return;
            }

            var fetchXml = $@"
                <fetch>
                  <entity name='msevtmgt_eventregistration'>
                    <attribute name='msevtmgt_eventregistrationid' />
                    <filter>
                      <condition attribute='msevtmgt_eventid' operator='eq' value='{eventId}' />
                    </filter>
                    <link-entity name='contact' from='contactid' to='msevtmgt_contactid' link-type='inner'>
                      <filter>
                        <condition attribute='emailaddress1' operator='eq' value='{emailRaw}' />
                      </filter>
                    </link-entity>
                  </entity>
                </fetch>";

            tracing.Trace("Running duplicate check with FetchXML:");
            tracing.Trace(fetchXml);

            bool alreadyRegistered = false;
            try
            {
                var result = service.RetrieveMultiple(new FetchExpression(fetchXml));
                alreadyRegistered = (result != null && result.Entities != null && result.Entities.Count > 0);
                tracing.Trace($"Duplicate check result: alreadyRegistered={alreadyRegistered}");
            }
            catch (Exception ex)
            {
                tracing.Trace("Error during duplicate check: " + ex.ToString());
            }

            var response = new ValidateFormSubmissionResponse
            {
                IsValid = !alreadyRegistered,
                ValidationOnlyFields = alreadyRegistered ? new List<string> { "emailaddress1" } : new List<string>()
            };

            context.OutputParameters["msdynmkt_validationresponse"] = Serialize(response);
        }
        private T Deserialize<T>(string jsonString)
        {
            var serializer = new DataContractJsonSerializer(typeof(T));
            T result;
            using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonString)))
            {
                result = (T)serializer.ReadObject(stream);
            }
            return result;
        }

        private string Serialize<T>(T obj)
        {
            string result;
            var serializer = new DataContractJsonSerializer(typeof(T));
            using (MemoryStream memoryStream = new MemoryStream())
            {
                serializer.WriteObject(memoryStream, obj);
                result = Encoding.Default.GetString(memoryStream.ToArray());
            }
            return result;
        }
        public class FormSubmissionRequest { public Dictionary<string, string> Fields { get; set; } }
        public class ValidateFormSubmissionResponse { public bool IsValid { get; set; } public List<string> ValidationOnlyFields { get; set; } }

    }
}

Paso 3: Registrar el Plugin

Siga estas directrices de Microsoft:

  • Nombre del paso: "msdynmkt_validateformsubmission"
  • Modo de ejecución: Synchronous
  • Orden de ejecución: 10 (para evitar conflictos con plugins de Microsoft)
  • Etapa de ejecución en la canalización: Post Operation

Paso 4: Proporcionar dinámicamente el GUID del evento en el formulario

Gracias a los formularios de marketing en tiempo real, ahora podemos insertar dinámicamente el ID del evento.
Crea un nuevo campo personalizado llamado “event”, establezce su valor con {{EventId}} y luego oculta el campo en el formulario.

Ejemplo HTML:

<div class="textFormFieldBlock" data-editorblocktype="TextFormField" data-prefill="false" data-hide="hide">
  <label title="Field label" for="shorttext564-1758226821033">
    <span class="msdynmkt_personalization">{{EventId}}</span><br>
  </label>
  <input id="shorttext564-1758226821033" type="text" name="event" value="{{EventId}}" maxlength="256">
</div>

Paso 5: Ajustar el mensaje de error

Es recomendable mostrar un mensaje de error claro y entendible para los usuarios. Puede añadir un script como este:

<script>
document.addEventListener("d365mkt-afterformsubmit", function(event) {
  if (!event.detail.successful) {  
  const baseText =  "Error submitting the form";
  const oneTimeMsg = "You can only register once";

  const selector = ".onFormSubmittedFeedbackInternalContainer[data-submissionresponse='error'] .onFormSubmittedFeedbackMessage";

  function applyOnce() {
    const msgEl = document.querySelector(selector);
    if (!msgEl) return false;

    const targetHTML = baseText + "<br>" + oneTimeMsg;
    if (msgEl.innerHTML.trim() !== targetHTML) {
      msgEl.innerHTML = targetHTML;
    }
    return true;
  }

  if (applyOnce()) return;

  const observer = new MutationObserver((_, obs) => {
    if (applyOnce()) {
      obs.disconnect();
    }
  });

  observer.observe(document.body, { childList: true, subtree: true });

  setTimeout(() => observer.disconnect(), 5000);
  }
});
</script>

Tip: No te olvides de insertar este Javascript entre las tags de Form en el HTML de tu formulario, Real Time Marketing requiere de esto para funcionar

¿Y si no sé programar?

No pasa nada: puedes descargar e instalar mi solución de ejemplo.
Esta revisará automáticamente si ya existe un contacto registrado con el mismo correo y mostrará un mensaje de error.

/posts/custom-form-submission-validation/image3.png
Este mensaje no es muy descriptivo...

Solo asegúrate de implementar el Paso 1 para que el plugin se ejecute.
Y si te animas, recomiendo mucho el Paso 5, ya que mejora bastante la experiencia del usuario 😉

/posts/custom-form-submission-validation/image4.png
Simple pero efectivo


👉 ¿Has probado este enfoque? Cuéntamelo en LinkedIn — ¡me encantaría conocer tu experiencia con los registros de eventos!