Member Registration in Umbraco 9

In this article, we will look into Member Registration in Umbraco 9. 

Member Registration in Umbraco 9

Umbraco 9 makes use of ASP.NET Core Identity for both backoffice users and website members. With previous versions of Umbraco, we had the MembershipHelper that was handy for accessing member data as IPublishedContent. The helper is available in Umbraco 9 as well. It contains a variety of helper methods that can be used in views and controllers. Umbraco 9 also comes with an interface IMemberManager that can help access member data as MemberIdentityUser

Let us start with the view model for registration, the code is as shown below.

public class RegisterViewModel
{
	public string Email { get; set; }

	public string Password { get; set; }

	public bool HasAPetUnicorn { get; set; }

	public string FirstName { get; set; }

	public string LastName { get; set; }
}

Now we need a ViewComponent to build up the form. The ViewComponent passes an instance of the RegisterViewModel to my view.

public class RegisterViewComponent : ViewComponent
{
	public IViewComponentResult Invoke()
	{
		return View(new RegisterViewModel());
	}
}

The View(Default.cshtml) for this ViewComponent is in the folder ~/Views/Components/Register. This is a bit of convention followed for ViewComponents. The code for the view is as shown below. 

@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<RegisterViewModel>

@using (Html.BeginUmbracoForm("Register", "AccountSurface", FormMethod.Post, new { @class = "text-left" }))
{
    <div class="form-floating">
        <input asp-for="@Model.FirstName" class="form-control" id="firstname" type="text" placeholder="Enter your first name..."  autocomplete="off" data-sb-validations="required" />
        <label for="Name">First Name</label>
        <span asp-validation-for="@Model.FirstName" class="text-danger"></span>
    </div>

     <div class="form-floating">
        <input asp-for="@Model.LastName" class="form-control" id="lastname" type="text" placeholder="Enter your last name..."  autocomplete="off" data-sb-validations="required" />
        <label for="Name">Last Name</label>
        <span asp-validation-for="@Model.LastName" class="text-danger"></span>
    </div>
    
    <div class="form-floating">
        <input asp-for="@Model.Email" class="form-control" id="email" placeholder="Enter your email..."  autocomplete="off" data-sb-validations="required,email"/>
        <label for="Email">Email</label>
        <span asp-validation-for="@Model.Email" class="text-danger"></span>
    </div>

    <div class="form-floating">
        <input asp-for="@Model.Password" class="form-control" type="password" id="password" placeholder="Enter your email..." autocomplete="off" data-sb-validations="required"/>
        <label for="Email">Password</label>
        <span asp-validation-for="@Model.Password" class="text-danger"></span>
    </div>

    <div class="form-floating">
        <input type="checkbox" asp-for="@Model.HasAPetUnicorn" >
        <label for="Message">Has a pet unicorn</label>
    </div>
    <br />
    <div class="form-floating">
        <button class="btn btn-primary text-uppercase float-end" id="register" type="submit">Register</button>
    </div>
}

I have a document type called Register which has a default template by the same name. It is a very basic document type. I can invoke my ViewComponent in the template as shown below.

 @await Component.InvokeAsync("Register")

The view posts back to an action called Register in the AccountSurfaceController. So let us have a look at the code for the action.

public class AccountSurfaceController : SurfaceController
    {
        private readonly IMemberService _memberService;
        private readonly IMemberManager _memberManager;
        private readonly IMemberSignInManager _memberSignInManager;
        private readonly IPublishedContentQuery _publishedContentQuery;
        private readonly ILogger<AccountSurfaceController> _logger;
        public AccountSurfaceController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider, IMemberService memberService, IMemberManager memberManager, IMemberSignInManager memberSignInManager, IPublishedContentQuery publishedContentQuery, ILogger<AccountSurfaceController> logger) : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
        {
            _memberService = memberService;
            _memberManager = memberManager;
            _memberSignInManager = memberSignInManager;
            _publishedContentQuery = publishedContentQuery;
            _logger = logger;
        }

        [HttpPost]
        public async Task<IActionResult> Register(RegisterViewModel model)
        {           
            if (!ModelState.IsValid)
            {
                return CurrentUmbracoPage();
            }

            //check whether the user is already registered
            if (await _memberManager.FindByEmailAsync(model.Email)  != null)
            {                
                TempData["Success"] = false;
                TempData["errorMessage"] = "User already registered!";
                return CurrentUmbracoPage();
            }

            //create a new identity member instance without an identity
            var identityMember = MemberIdentityUser.CreateNew(model.Email, model.Email, Models.Member.ModelTypeAlias, true, model.FirstName + " " + model.LastName);

            //create an identity member
            var identityResult = await _memberManager.CreateAsync(identityMember, model.Password);

            if (identityResult.Succeeded)
            {
                TempData["Success"] = true;

                if (model.HasAPetUnicorn)
                {
                    //add to role
                    await _memberManager.AddToRolesAsync(identityMember, new string[] { "unicorn user group" });
                   
                    //save the additional details using the MemberService
                    var member = _memberService.GetByKey(identityMember.Key);
                    member.SetValue("hasAUnicorn", model.HasAPetUnicorn);
                    _memberService.Save(member);
                }               
            }
            else
            {
                TempData["Success"] = false;

                var errors = identityResult.Errors;

                var errorString = new StringBuilder();

                //added for learning purposes. This could be logged
                foreach(var error in errors)
                {
                    errorString.Append($"{error.Code}-{error.Description}");
                }

                _logger.LogError(errorString.ToString());
            }

            return CurrentUmbracoPage();
        }
}

I am heavily making use of the IMemberManager interface in my code as you can see. I have also added some TempData goodness to show any messages to the end user.

REST : Myths & Facts

01 December 2023

This article discusses some of the misconceptions about REST, and what is not REST and presents you with the facts.