Inhalt

So verhindert man doppelte Event-Registrierungen in Dynamics 365 Customer Insights (mit Formular-Validierung)

Vor einigen Monaten wurde ich gefragt, ob es eine Möglichkeit gibt, die Registrierungen für ein Event in Dynamics 365 Customer Insights Journeys auf eine Anmeldung pro Person zu beschränken. Damals dachte ich, dass es dafür keine Einstellung gibt. Also habe ich nachgeforscht – und bin schliesslich auf diese Microsoft-Dokumentation gestoßen: Customize form submission validation

Dabei kam mir der Gedanke: Wenn es möglich ist, ein Formular vor dem Absenden zu validieren, dann können wir doppelte Registrierungen ganz verhindern. Nach einigen Tests habe ich ein Plugin entwickelt, das die Event-Registrierung beim Absenden des Formulars überprüft und den Vorgang blockiert, falls für dieselbe Person bereits eine Anmeldung existiert.

„Ja gut, Martin, aber was bedeutet das genau?“

Kurz gesagt: Man kann den Registrierungsprozess so einschränken, dass geprüft wird, ob diese Person bereits am Event angemeldet ist

WIE?

Schritt 1: data-form-validation im Registrierungsformular aktivieren

Alles beginnt damit, im Formular das Attribut data-validate-submission="true" zu setzen:

/posts/custom-form-submission-validation/image1.png
Füg im Form-Tag Ihres Formulars einfach data-validate-submission='true' hinzu

Ohne diesen Schritt wird Ihr Plugin nicht ausgelöst, da dies die Registrierung unter der Nachricht "msdynmkt_validateformsubmission" ermöglicht.

Beispiel:

<form aria-label="{{EventName}}" class="marketingForm" data-validate-submission="true"
  data-successmessage="Danke für Ihre Anmeldung." data-errormessage="Es ist ein Fehler aufgetreten, bitte versuchen Sie es erneut."
  data-waitlistmessage="Sie wurden auf die Warteliste gesetzt.">

Schritt 2: Plugin erstellen

Das Plugin deserialisiert die Formularparameter, prüft anhand der E-Mail und der Event-ID, ob bereits eine Registrierung existiert, und blockiert ggf. die neue Anmeldung.

Für alle Low-Code/No-Code-Nutzer hier in einfachen Worten:
👉 Das Plugin liest die Formular-Daten, überprüft, ob die E-Mail schon für das Event registriert ist, und stoppt die erneute Anmeldung.

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; } }

    }
}

Schritt 3: Plugin registrieren

Achte darauf, die Microsoft-Vorgaben einzuhalten:

  • Step-Name: "msdynmkt_validateformsubmission"
  • Execution Mode: Synchronous
  • Execution Order: 10 (verhindert Konflikte mit Microsoft-Plugins)
  • Pipeline Stage: Post Operation

Schritt 4: Event-GUID dynamisch ins Formular einfügen

Mit Real-Time-Marketing-Formularen können wir die Event-ID dynamisch hinzufügen.
Dazu erstellt man ein neues benutzerdefiniertes Feld mit dem Namen „event“ und füllt es mit dem Platzhalter {{EventId}}. Danach kann man das Feld im Formular verstecken.

Beispiel-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>

Schritt 5: Fehlermeldung für Nutzer verständlich machen

Damit die Benutzer verstehen, warum ihre Anmeldung blockiert wurde, sollte man das Formular-HTML anpassen. Beispiel-Skript:

<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: Vergiss nicht, den Javascript zwischen den Form Tags in dein Form HTML hinzuzufügen. Real Time Marketing benötigt dies um zu funktionieren.

Aber ich kann nicht programmieren, was nun?

Kein Problem – Du kannstn auch meine Beispiel-Lösung herunterladen und installieren.
Damit wird automatisch geprüft, ob bereits eine Kontaktperson mit derselben E-Mail-Adresse registriert ist. Falls ja, erhält der Benutzer eine Fehlermeldung.

/posts/custom-form-submission-validation/image3.png
Diese Meldung ist nicht besonders hilfreich...

Wenn du Schritt 1 umsetzst, wird das Plugin ausgelöst. Und wenn du mutig bist, empfehle ich unbedingt Schritt 5, um die Benutzererfahrung deutlich zu verbessern 😉

/posts/custom-form-submission-validation/image4.png
Einfach, aber effektiv


👉 Habt Ihr diesen Ansatz ausprobiert? Lass es mich auf LinkedIn wissen – ich freue mich auf den Austausch!