Email confirmation for Umbraco Members

I am working on an application that requires email verification for members registering in Umbraco. I’ve explored some options for how to implement this, including adding a MemberSavedNotificationHandler, or creating a custom registration controller endpoint instead of UmbRegisterController. I’d really rather use UmbRegisterController, that way the process is the same whether creating a user in the backoffice or from the registration page. And it would make keeping registration logic simple and easy when Umbraco upgrades happen.

But as I’ve been exploring the possibility of using a notification, I keep running into an issue where I am not able to check if a member is new or not. I have tried IsPropertyDirty, WasPropertyDirty for Id, but these both return false. I have also tried checking if CreateDate equals UpdateDate, but this also does not work.

Is there any way you know of to make this possible in a notification or should I resign myself to creating a registration controller?

Instead of “guys”, may we suggest, for example: “folks", “Umbracians”, “all”, or “everyone”? We use gender inclusive language in this forum :grinning_face: (read more)

1 Like

Hi @grrtt49

The MemberSavedNotificationHandler is called after the member is saved, so you probably cannot check from there if the member being saved was new or not. You would need to use the MemberSavingNotificationHandler, this happens before the save so you can check for dirty properties and a default ID. The only issue here is that if there is an issue saving, you may have already sent an email if the save fails.

Personally, I would create a new surface controller that duplicates the UmbRegisterController and add your email sending logic into the action after successful registration. See the built in controller here.

Another option may be to write an IAsyncActionFilter to keep using the UmbRegisterController.

Here is the AI suggestion which I’ve not tested.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Web.Website.Controllers;
using Umbraco.Cms.Web.Website.Models;

namespace YourProject.Members;

public sealed class RegistrationCompletedFilter : IAsyncActionFilter
{
    private readonly ILogger<RegistrationCompletedFilter> _logger;

    public RegistrationCompletedFilter(ILogger<RegistrationCompletedFilter> logger)
        => _logger = logger;

    public async Task OnActionExecutionAsync(
        ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var executed = await next();

        if (executed.Exception is not null || !executed.ModelState.IsValid)
            return;

        var succeeded = executed.Result is RedirectResult or RedirectToRouteResult
                        || (context.Controller is Controller c
                            && c.TempData["FormSuccess"] is true);

        if (!succeeded) return;

        if (context.ActionArguments.TryGetValue("model", out var arg)
            && arg is RegisterModel model)
        {
            _logger.LogInformation(
                "Member registration completed for {Email}", model.Email);

            // post-registration logic here
        }
    }
}

public sealed class RegisterFilterConvention : IControllerModelConvention
{
    public void Apply(ControllerModel controller)
    {
        if (controller.ControllerType != typeof(UmbRegisterController)) return;

        var action = controller.Actions.FirstOrDefault(a =>
            a.ActionName == nameof(UmbRegisterController.HandleRegisterMember));

        action?.Filters.Add(new TypeFilterAttribute(typeof(RegistrationCompletedFilter)));
    }
}

public sealed class RegisterFilterComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.Services.Configure<MvcOptions>(o =>
            o.Conventions.Add(new RegisterFilterConvention()));
    }
}

Justin

Hi @grrtt49 ,
Sharing some code. Hope it helps!

using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;

namespace MyQuickUmbracoSitetestsql10.Handler;

public class MemberVerificationHandler : 
    INotificationHandler<MemberSavingNotification>, 
    INotificationHandler<MemberSavedNotification>
{
    private readonly ILogger<MemberVerificationHandler> _logger;

    public MemberVerificationHandler(ILogger<MemberVerificationHandler> logger)
    {
        _logger = logger;
    }

    // Step 1: Catch new members before DB save
    public void Handle(MemberSavingNotification notification)
    {
        foreach (var member in notification.SavedEntities)
        {
            if (!member.HasIdentity)
            {
                _logger.LogInformation("SUCCESS: Brand new member detected: {Email}", member.Email);
                
                // Force unapproved status
                member.IsApproved = false;

                // Pass flag to Step 2
                notification.State["IsNewMember"] = true;
            }
        }
    }

    // Step 2: Trigger email after DB save
    public void Handle(MemberSavedNotification notification)
    {
        if (notification.State.ContainsKey("IsNewMember"))
        {
            foreach (var member in notification.SavedEntities)
            {
                _logger.LogInformation("SUCCESS: Ready to send verification email for: {Email}", member.Email);
                
                // Email logic goes here
            }
        }
    }
}

And the composer to wire it up:

using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Notifications;

namespace MyQuickUmbracoSitetestsql10.Handler;

public class MemberComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.AddNotificationHandler<MemberSavingNotification, MemberVerificationHandler>();
        builder.AddNotificationHandler<MemberSavedNotification, MemberVerificationHandler>();
    }
}

How to test it:

  1. Backoffice Create: Make a new member manually. Check the Log Viewer for the “Brand new member” message, and verify their “Approved” box gets unchecked automatically.
  2. Backoffice Edit: Open an existing member, change a property, and save. It shouldn’t touch their approved status or trigger the new member logs.

That’s interesting, I didn’t know you could store state between ‘ing’ and ‘ed’ handlers… thanks @ShekharTarare

1 Like

Not sure that will work, unless HQ have backtracked on suppressing multiple notifications when members are created..

I admit I’ve not tested your code… :slight_smile:

Determining if Member is New in the MemberSavedNotification? · Issue #20071 · umbraco/Umbraco-CMS

As far as I understand it the first member notification you get is when properties are updated (the second fire during member creation, with the initial new member create notification being supressed) and at that point you will have an HasIdentity will be true?

1 Like

Thanks for pointing that out @mistyn8 . You’re correct, the first notification will fire but not the other one.

We are left with the first notification only. It can be done like this, but there’s risk on this, it will send an email when data was actually getting saved. If anything happens while saving in DB then an email will be sent for the user who doesn’t exist.

using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;

namespace MyQuickUmbracoSitetestsql10.Handler
{
    public class MemberVerificationHandler : INotificationHandler<MemberSavingNotification>
    {
        private readonly ILogger<MemberVerificationHandler> _logger;

        public MemberVerificationHandler(ILogger<MemberVerificationHandler> logger)
        {
            _logger = logger;
        }

        public void Handle(MemberSavingNotification notification)
        {
            foreach (var member in notification.SavedEntities)
            {
                // If they don't have an ID, they are 100% brand new
                if (!member.HasIdentity)
                {
                    _logger.LogInformation("Brand new member detected: {Email}", member.Email);

                    // 1. Keep them from logging in
                    member.IsApproved = false;

                    // 2. Just fire your email logic right here and be done with it.
                    // NOTE: member.Id will be 0 here, so if your email link relies on the DB ID, 
                    // you will need to generate a custom token or use their Email/Username instead.
                }
            }
        }
    }
}

Please check this for extra info: Member Registration and Login | CMS | Umbraco Documentation