Back to: ASP.NET Core Tutorials For Beginners and Professionals
Gmail-Like Registration System using ASP.NET Core MVC
Creating a user-friendly multi-step registration process similar to Google’s Gmail registration involves several components, including multiple views, models, controllers, services, and client-server interactions that collect user information step-by-step. This approach enhances the user experience by breaking down complex forms into manageable stages and enabling validations at each point, thereby reducing errors and increasing successful registrations.
Key Technologies Used:
We will use the following technology stack to develop the application.
- ASP.NET Core MVC: For routing, views, controllers, and model binding.
- Entity Framework Core: Code-First approach for database design and interaction.
- SQL Server: The backend database to manage the data permanently.
- Remote Validation and Data Annotations: For real-time, field-specific validation, to ensure both client-side and server-side data integrity.
Project Overview
The registration process will be divided into ten distinct steps, each focusing on a specific subset of user data and validation requirements. This modular approach mimics Gmail’s own flow, providing a familiar and efficient user journey. The steps are as follows:
- Step 1: Enter First Name and Last Name
- Step 2: Enter Date of Birth and Gender
- Step 3: Choose a Gmail Address
- Step 4: Create a Strong Password
- Step 5: Verify Phone Number
- Step 6: Enter Verification Code
- Step 7: Add Recovery Email (Optional)
- Step 8: Review Account Information
- Step 9: Accept Privacy and Terms
- Step 10: Create Account and Show Dashboard
Each step will have its own view and corresponding backend logic to handle data and validations. Before implementing, let’s first review the pages and flow that we will develop as part of our Gmail-like registration system.
Step 1: Enter First Name and Last Name
The first interaction with the registration form asks users to input their first and last names. This information is mandatory and must be carefully validated to prevent invalid inputs such as numbers or special characters, which could cause database inconsistencies.
- Both fields are required.
- Inputs are restricted to the alphabet and standard name characters.
- The use of Data Annotations, such as [Required] and custom regular expression validation, ensures that only valid names are accepted.
- After submission, backend validation confirms the data before proceeding to the next step.
This step sets the tone for a seamless and error-free user experience from the outset of the registration process.
Step 2: Enter Date of Birth and Gender
In this step, the registration form collects the user’s date of birth and gender information. The date input is split into three separate fields, a month dropdown, and day and year textboxes, to minimize entry errors and simplify validation.
- Using a dropdown for the month ensures a valid month selection.
- Day and year inputs are validated to prevent impossible dates (e.g., Feb 30th).
- Gender selection is restricted to predefined values (Male, Female, Other), which can be extended as needed.
- Validations ensure that users meet the minimum age requirements, if applicable.
This step adds important demographic data critical for compliance and legal purposes.
Step 3: Choose a Gmail Address
This step involves selecting or creating an email. Selecting a unique email address is a crucial step in the registration process. The system will provide suggested usernames based on the user’s name and allow the creation of a custom email address.
- Client-side checks using Remote Validation confirm that the desired email address is not already taken by someone else.
- Real-time feedback enhances usability by preventing the submission of duplicate emails.
- The backend validates email format and uniqueness as a final safeguard.
By preventing duplicate email registrations upfront, we reduce user frustration and backend conflict. This will enhance the user’s experience by preventing registration delays due to email duplication.
Step 4: Create a Strong Password
Security is paramount; therefore, the user is prompted to create a strong password that meets strict criteria.
- Minimum length of 8 characters
- Must include uppercase, lowercase, numeric, and special characters
- Password strength feedback can be provided via a dynamic progress bar or indicators
- Users can toggle password visibility to verify input accuracy
Implementing these measures ensures compliance with modern security best practices and reduces the risk of account compromise.
Step 5: Verify Phone Number
Adding a phone number adds an extra layer of account security and recovery capability.
- Users input their mobile number with format validation based on the country code.
- The system sends a One-Time Password (OTP) for verification via SMS
- This step is mandatory to link the phone number with the account
Phone verification strengthens account authenticity and facilitates multi-factor authentication.
Step 6: Enter Verification Code
Users enter the OTP received on their phone to confirm ownership of the mobile number provided.
- The verification input includes timers and resend options for usability
- Backend confirms the OTP matches the sent code and is within the valid timeframe
- On successful validation, the process proceeds
This multi-factor authentication step is a critical security enhancement.
Step 7: Add Recovery Email (Optional)
Users can provide a secondary email address to help recover their account in the case of forgot password.
- Optional step: Users may skip if desired
- Email format validation is enforced
- Provides an additional layer of account recovery security
This flexibility strikes a balance between user convenience and account security.
Step 8: Review Account Information
Before final submission, users are presented with a summary page that lists all the information they have entered.
- Enables correction of errors before finalization
- Offers a confidence check that all data is accurate
- Links to previous steps allow easy navigation back to edit information
Review pages reduce user errors and increase successful registrations.
Step 9: Accept Privacy and Terms
Users must agree to the platform’s Privacy Policy and Terms of Service before creating their account.
- The agreement checkbox is mandatory
- Links to detailed policies open in modals or new tabs for review
- Legal requirement to ensure users’ consent to data use policies
This step ensures regulatory compliance and builds user trust.
Step 10: Create Account and Show Dashboard
Upon accepting the terms, the system completes account creation by securely storing the data and initializing the user’s session.
- Users are redirected to their personalized dashboard
- The dashboard displays profile info and welcome messages
- Users can start exploring features immediately
This final step marks the successful completion of registration and entry into the application ecosystem.
These steps outline a comprehensive process for user registration, mirroring the flow of Google’s Gmail registration.
Creating a New ASP.NET Core MVC Project
Let’s start by creating a new ASP.NET Core web application using the Model-View-Controller (MVC) template, which provides a structured framework that separates the application into three main parts: Models, Views, and Controllers. Name the project GmailRegistrationDemo.
Next, install essential NuGet packages that enable key functionalities:
- Microsoft.EntityFrameworkCore.SqlServer and Microsoft.EntityFrameworkCore.Tools: These are required for integrating SQL Server as your database provider and enabling migrations for managing database schema changes smoothly.
- libphonenumber-csharp: A C# port of Google’s libphonenumber library, this package ensures robust and accurate international phone number parsing, validation, and formatting.
- BCrypt.Net-Next: Used for secure password hashing, protecting user passwords by hashing them with a strong algorithm before storing in the database.
You can install these packages via the NuGet Package Manager GUI or by running commands in the Package Manager Console, ensuring your project has all the necessary libraries to implement the required features securely and efficiently.
- Install-Package Microsoft.EntityFrameworkCore.SqlServer
- Install-Package Microsoft.EntityFrameworkCore.Tools
- Install-Package libphonenumber-csharp
- Install-Package BCrypt.Net-Next
Defining the Data Models
Create a dedicated folder named Models at the project root to keep our domain and data entities organized.
Gender Enum:
Create a class file named Gender.cs within the Models folder and then copy and paste the following code. Using an enum enforces type safety, ensuring that gender values are always valid and meaningful within the system, thereby avoiding string typos and inconsistent values. This enum defines the possible gender values (Male, Female, Other) used for the user’s gender field.
namespace GmailRegistrationDemo.Models { // Enum for Gender public enum Gender { Male, Female, Other } }
User Entity
Create a class file named User.cs within the Models folder and then copy and paste the following code. It defines the user entity saved in the database, including properties like Email, Password (hashed), First and Last Name, Date of Birth, Gender, Country Code, Phone Number, Recovery Email, and creation timestamp.
using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; namespace GmailRegistrationDemo.Models { [Index(nameof(Email), Name = "UX_Users_Email_Unque", IsUnique = true)] public class User { [Key] public int Id { get; set; } [Required] public string Email { get; set; } = null!; [Required] public string PasswordHash { get; set; } = null!; [Required] [StringLength(50)] public string FirstName { get; set; } = null!; [Required] [StringLength(50)] public string LastName { get; set; } = null!; [Required] public DateTime DateOfBirth { get; set; } [Required] public Gender Gender { get; set; } [Required] public string CountryCode { get; set; } = null!; [Required] [Phone] public string PhoneNumber { get; set; } = null!; [EmailAddress] public string? RecoveryEmail { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime? LastUpdated { get; set; } } }
RegistrationSession Model
Create a class file named RegistrationSession.cs within the Models folder and then copy and paste the following code. It is a temporary data structure used to store the state of the registration process (all entered information) across multiple steps before a user is fully registered. This enables a smooth multi-step experience without prematurely saving incomplete data to the Users table.
using System.ComponentModel.DataAnnotations; namespace GmailRegistrationDemo.Models { public class RegistrationSession { [Key] public Guid RegistrationId { get; set; } = Guid.NewGuid(); public string? FirstName { get; set; } public string? LastName { get; set; } public string? Email { get; set; } public DateTime? DateOfBirth { get; set; } public Gender? Gender { get; set; } public string? PasswordHash { get; set; } public string? CountryCode { get; set; } public string? PhoneNumber { get; set; } public string? RecoveryEmail { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime LastUpdated { get; set; } = DateTime.UtcNow; } }
Create Registration View Models for Each Step
Let us proceed and create the View Models for each step. First, create a folder named ‘ViewModels’ in the project root directory, where we will store all our View Models.
Step 1: View Model for First and Last Name
Create a class file named Step1ViewModel.cs within the ViewModels folder and then copy and paste the following code. This View Model captures the user’s first and last names during the initial registration step.
using System.ComponentModel.DataAnnotations; namespace GmailRegistrationDemo.ViewModels { // ViewModel for Step 1: Enter First and Last Name. public class Step1ViewModel { [Required(ErrorMessage = "First Name is required.")] [RegularExpression(@"^[a-zA-Z]+$", ErrorMessage = "First name must contain only letters.")] [Display(Name = "First Name")] public string FirstName { get; set; } = null!; [Required(ErrorMessage = "Last Name is required.")] [RegularExpression(@"^[a-zA-Z]+$", ErrorMessage = "Last name must contain only letters.")] [Display(Name = "Last Name")] public string LastName { get; set; } = null!; } }
Step 2: Date of Birth and Gender View Model
Create a class file named Step2ViewModel.cs within the ViewModels folder and then copy and paste the following code. This View Model collects the user’s date of birth and gender information in the second registration step.
using System.ComponentModel.DataAnnotations; using GmailRegistrationDemo.Models; namespace GmailRegistrationDemo.ViewModels { // ViewModel for Step 2: Enter Date of Birth and Gender. public class Step2ViewModel { [Required(ErrorMessage = "Month is required.")] public string Month { get; set; } = null!; [Required(ErrorMessage = "Day is required.")] [Range(1, 31, ErrorMessage = "Please enter a valid day.")] public int Day { get; set; } [Required(ErrorMessage = "Year is required.")] [Range(1900, 2100, ErrorMessage = "Please enter a valid year.")] public int Year { get; set; } [Required(ErrorMessage = "Gender is required.")] public Gender Gender { get; set; } } }
Step 3: Choose Gmail Address View Model
Create a class file named Step3ViewModel.cs within the ViewModels folder and then copy and paste the following code. This View Model allows the user to choose a unique Gmail address and validate its availability.
using Microsoft.AspNetCore.Mvc; using System.ComponentModel.DataAnnotations; namespace GmailRegistrationDemo.ViewModels { // ViewModel for Step 3: Choose Gmail Address. public class Step3ViewModel { [Required(ErrorMessage = "Gmail address is required.")] [EmailAddress(ErrorMessage = "Please enter a valid email address.")] public string Email { get; set; } = null!; // Initialize SuggestedEmails to prevent null references public List<string> SuggestedEmails { get; set; } = new List<string>(); // Selected Suggested Email public string? SuggestedEmail { get; set; } // Custom Email Input [EmailAddress(ErrorMessage = "Please enter a valid email address.")] public string? CustomEmail { get; set; } } }
Step 4: Create Password View Model
Create a class file named Step4ViewModel.cs within the ViewModels folder and then copy and paste the following code. This View Model enables the user to create a strong password with confirmation and toggle visibility.
using System.ComponentModel.DataAnnotations; namespace GmailRegistrationDemo.ViewModels { // ViewModel for Step 4: Create a Strong Password. public class Step4ViewModel { [Required(ErrorMessage = "Password is required.")] [DataType(DataType.Password)] [MinLength(8, ErrorMessage = "Password must be at least 8 characters long.")] [RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@#$!%*?&])[A-Za-z\d@#$!%*?&]{8,}$", ErrorMessage = "Password must include uppercase, lowercase, number, and special character.")] public string Password { get; set; } = null!; [Required(ErrorMessage = "Confirm Password is required.")] [DataType(DataType.Password)] [Compare("Password", ErrorMessage = "Passwords do not match.")] [Display(Name = "Confirm Password")] public string ConfirmPassword { get; set; } = null!; // Property to toggle password visibility [Display(Name = "Show Password")] public bool ShowPassword { get; set; } } }
Step 5: Phone Number View Model
Create a class file named Step5ViewModel.cs within the ViewModels folder and then copy and paste the following code. This view model collects the user’s phone number for verification purposes.
using Microsoft.AspNetCore.Mvc.Rendering; using PhoneNumbers; using System.ComponentModel.DataAnnotations; namespace GmailRegistrationDemo.ViewModels { // ViewModel for Step 5: Enter Phone Number. // Implements IValidatableObject for custom phone number validation. public class Step5ViewModel : IValidatableObject { [Required(ErrorMessage = "Country code is required.")] [Display(Name = "Country")] public string CountryCode { get; set; } = null!; [Required(ErrorMessage = "Phone number is required.")] [Display(Name = "Phone Number")] public string PhoneNumber { get; set; } = null!; // List of country codes for the dropdown (populated in controller or view) public IEnumerable<SelectListItem>? CountryCodes { get; set; } // Custom validation for phone number based on country code using libphonenumber-csharp public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { var results = new List<ValidationResult>(); if (string.IsNullOrEmpty(CountryCode)) { results.Add(new ValidationResult("Country code must be selected.", new[] { nameof(CountryCode) })); return results; } if (string.IsNullOrEmpty(PhoneNumber)) { results.Add(new ValidationResult("Phone number is required.", new[] { nameof(PhoneNumber) })); return results; } var phoneUtil = PhoneNumberUtil.GetInstance(); // Remove whitespace for safety var nationalNumber = PhoneNumber.Trim(); // If user mistakenly adds "+" or "00" prefix, remove it if (nationalNumber.StartsWith("+")) nationalNumber = nationalNumber.Substring(1); if (nationalNumber.StartsWith("00")) nationalNumber = nationalNumber.Substring(2); // Remove country code prefix if user included it by mistake if (nationalNumber.StartsWith(CountryCode.Replace("+", ""))) nationalNumber = nationalNumber.Substring(CountryCode.Replace("+", "").Length); // Map calling code to region var callingCodeToRegion = new Dictionary<string, string> { { "+1", "US" }, { "+44", "GB" }, { "+91", "IN" } // Add more as needed }; if (!callingCodeToRegion.TryGetValue(CountryCode, out var regionCode)) { results.Add(new ValidationResult("Unsupported country code.", new[] { nameof(CountryCode) })); return results; } try { // Parse expects only the national number and the region code var parsedNumber = phoneUtil.Parse(nationalNumber, regionCode); if (!phoneUtil.IsValidNumber(parsedNumber)) { results.Add(new ValidationResult("Please enter a valid phone number.", new[] { nameof(PhoneNumber) })); } } catch (NumberParseException) { results.Add(new ValidationResult("Invalid phone number format.", new[] { nameof(PhoneNumber) })); } return results; } // Static method to provide country codes list for dropdowns public static List<SelectListItem> GetCountryCodes() { return new List<SelectListItem> { new SelectListItem { Value = "+1", Text = "United States (+1)" }, new SelectListItem { Value = "+44", Text = "United Kingdom (+44)" }, new SelectListItem { Value = "+91", Text = "India (+91)" }, // Add more countries as needed }; } } }
Step 6: Phone Verification View Model
Create a class file named Step6ViewModel.cs within the ViewModels folder and then copy and paste the following code. This View Model receives and validates the verification code sent to the user’s phone.
using System.ComponentModel.DataAnnotations; namespace GmailRegistrationDemo.ViewModels { // ViewModel for Step 6: Enter Verification Code. public class Step6ViewModel { [Required(ErrorMessage = "Verification code is required.")] [RegularExpression(@"^\d{6}$", ErrorMessage = "Please enter a valid 6-digit code.")] [Display(Name = "Verification Code")] public string VerificationCode { get; set; } = null!; } }
Step 7: Add Recovery Email View Model
Create a class file named Step7ViewModel.cs within the ViewModels folder and then copy and paste the following code. This View Model optionally collects a recovery email address from the user.
using System.ComponentModel.DataAnnotations; namespace GmailRegistrationDemo.ViewModels { // ViewModel for Step 7: Add Recovery Email. public class Step7ViewModel { [EmailAddress(ErrorMessage = "Please enter a valid recovery email address.")] [Display(Name = "Recovery Email (Optional)")] public string? RecoveryEmail { get; set; } } }
Step 8: Review Account Info View Model
Create a class file named Step8ViewModel.cs within the ViewModels folder and then copy and paste the following code. This View Model presents a summary of the entered account information for user review before final submission.
namespace GmailRegistrationDemo.ViewModels { // ViewModel for Step 8: Review Account Information. public class Step8ViewModel { public string FullName { get; set; } = null!; public string Email { get; set; } = null!; public string Gender { get; set; } = null!; public DateTime DateOfBirth { get; set; } public string PhoneNumber { get; set; } = null!; public string? RecoveryEmail { get; set; } } }
Step 9: Privacy and Terms View Model
Create a class file named Step9ViewModel.cs within the ViewModels folder and then copy and paste the following code. This View Model ensures that the user agrees to the Privacy Policy and Terms of Service before creating an account.
using System.ComponentModel.DataAnnotations; namespace GmailRegistrationDemo.ViewModels { // ViewModel for Step 9: Privacy and Terms. public class Step9ViewModel { [Required(ErrorMessage = "You must agree to the terms and privacy policy to proceed.")] [Display(Name = "I agree to the Terms and Privacy Policy")] public bool Agree { get; set; } } }
Configuring the Database Context
First, create a folder named ‘Data’ and, inside it, create a class file named ‘GmailDBContext.cs’. Then, copy and paste the following code. This is the Entity Framework Core DbContext class that manages database access. It defines the Users and RegistrationSessions tables and configures the Gender enum to be stored as a string in the database. This class enables the application to perform CRUD (Create, Read, Update, Delete) operations on user and registration session data.
using GmailRegistrationDemo.Models; using Microsoft.EntityFrameworkCore; namespace GmailRegistrationDemo.Data { public class GmailDBContext : DbContext { public GmailDBContext(DbContextOptions<GmailDBContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { // Configure Gender to be stored as string modelBuilder.Entity<User>() .Property(u => u.Gender) .HasConversion<string>() .IsRequired(); } public DbSet<User> Users { get; set; } public DbSet<RegistrationSession> RegistrationSessions { get; set; } } }
Creating Services
Services help in encapsulating business logic. First, create a folder named Services at the project root directory.
- IRegistrationSessionService and RegistrationSessionService manage the lifecycle of the RegistrationSession entities, handling creation, retrieval, update, and deletion of temporary registration data.
- IGenerateEmailSuggestions and GenerateEmailSuggestions provide algorithms to generate unique Gmail address suggestions by appending random numbers and ensuring uniqueness in the database.
- IPhoneVerification and PhoneVerification handle sending and validating SMS verification codes, using an in-memory dictionary to track codes, expiration, and attempt limits.
IRegistrationSessionService
Create a class file named IRegistrationSessionService.cs within the Services folder and then copy and paste the following code.
using GmailRegistrationDemo.Models; namespace GmailRegistrationDemo.Services { public interface IRegistrationSessionService { Task<RegistrationSession> GetOrCreateSessionAsync(string? registrationId); Task<RegistrationSession> CreateNewSessionAsync(); Task SaveChangesAsync(); Task DeleteSessionAsync(RegistrationSession session); } }
RegistrationSessionService
Create a class file named RegistrationSessionService.cs within the Services folder and then copy and paste the following code. This service manages a server-side registration session, storing partial user data between steps, identified by a cookie named RegistrationId. This approach allows the registration process to be resumed without data loss across multiple requests
using GmailRegistrationDemo.Data; using GmailRegistrationDemo.Models; using Microsoft.EntityFrameworkCore; namespace GmailRegistrationDemo.Services { public class RegistrationSessionService : IRegistrationSessionService { private readonly GmailDBContext _context; private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger<RegistrationSessionService> _logger; public RegistrationSessionService( GmailDBContext context, IHttpContextAccessor httpContextAccessor, ILogger<RegistrationSessionService> logger) { _context = context; _httpContextAccessor = httpContextAccessor; _logger = logger; } public async Task<RegistrationSession> GetOrCreateSessionAsync(string? registrationId) { try { if (!string.IsNullOrEmpty(registrationId) && Guid.TryParse(registrationId, out var regId)) { var existing = await _context.RegistrationSessions.FindAsync(regId); if (existing != null) return existing; } return await CreateNewSessionAsync(); } catch (Exception ex) { _logger.LogError(ex, "Failed to get or create registration session."); throw; } } public async Task<RegistrationSession> CreateNewSessionAsync() { var newSession = new RegistrationSession { RegistrationId = Guid.NewGuid(), CreatedAt = DateTime.UtcNow, LastUpdated = DateTime.UtcNow, FirstName = string.Empty, LastName = string.Empty, Email = string.Empty, CountryCode = string.Empty, PhoneNumber = string.Empty, RecoveryEmail = string.Empty }; _context.RegistrationSessions.Add(newSession); await _context.SaveChangesAsync(); var options = new CookieOptions { HttpOnly = true, Expires = DateTimeOffset.UtcNow.AddHours(1) }; _httpContextAccessor.HttpContext?.Response.Cookies.Append("RegistrationId", newSession.RegistrationId.ToString(), options); return newSession; } public async Task SaveChangesAsync() { await _context.SaveChangesAsync(); } public async Task DeleteSessionAsync(RegistrationSession session) { var sessionUser =await _context.RegistrationSessions.FirstOrDefaultAsync(reg => reg.RegistrationId == session.RegistrationId); if (sessionUser != null) { _context.RegistrationSessions.Remove(sessionUser); //DELETED await _context.SaveChangesAsync(); _httpContextAccessor.HttpContext?.Response.Cookies.Delete("RegistrationId"); } } } }
IGenerateEmailSuggestions
Create a class file named IGenerateEmailSuggestions.cs within the Services folder and then copy and paste the following code.
namespace GmailRegistrationDemo.Services { public interface IGenerateEmailSuggestions { Task<List<string>> GenerateUniqueEmailsAsync(string baseEmail, int count = 2); } }
GenerateEmailSuggestions
Create a class file named GenerateEmailSuggestions.cs within the Services folder and then copy and paste the following code.
using GmailRegistrationDemo.Data; using Microsoft.EntityFrameworkCore; namespace GmailRegistrationDemo.Services { // Service for generating unique email suggestions. // This service is responsible for generating a list of unique email suggestions public class GenerateEmailSuggestions : IGenerateEmailSuggestions { private readonly GmailDBContext _context; public GenerateEmailSuggestions(GmailDBContext context) { _context = context; } // Asynchronously generates a list of unique email suggestions based on the base email provided // baseEmail: The base email to generate suggestions from (e.g., pranaya.rout@example.com) // count: The number of unique email suggestions to generate (default is 2) public async Task<List<string>> GenerateUniqueEmailsAsync(string baseEmail, int count = 2) { var suggestions = new List<string>(); // List to store email suggestions // If the base email is null or doesn't contain the '@' symbol, return an empty list if (string.IsNullOrEmpty(baseEmail) || !baseEmail.Contains("@")) return suggestions; // pranaya.rout@example.com // Split the base email into prefix and domain (e.g., "pranaya.rout" and "example.com") string emailPrefix = baseEmail.Split('@')[0]; // Extracts the part before '@' string emailDomain = baseEmail.Split('@')[1]; // Extracts the part after '@' string suggestion; // Variable to store the generated suggestion // Continue generating suggestions until we have the desired number (specified by 'count') while (suggestions.Count < count) { do { // Generate a random suggestion by appending a random number (100-999) to the prefix // pranaya.rout124@example.com suggestion = $"{emailPrefix}{new Random().Next(100, 999)}@{emailDomain}"; // Use AnyAsync to asynchronously check if the email already exists in the database // Also ensure that the suggestion is not already in the suggestions list } while (await _context.Users.AnyAsync(u => u.Email == suggestion) || suggestions.Contains(suggestion)); // Add the new unique suggestion to the list suggestions.Add(suggestion); } // Return the list of unique email suggestions return suggestions; } } }
IPhoneVerification
Create a class file named IPhoneVerification.cs within the Services folder and then copy and paste the following code.
namespace GmailRegistrationDemo.Services { public interface IPhoneVerification { Task<string> SendVerificationCodeAsync(string phoneNumber); bool ValidateCode(string phoneNumber, string code); } }
PhoneVerification
Create a class file named PhoneVerification.cs within the Services folder and then copy and paste the following code.
using System.Collections.Concurrent; namespace GmailRegistrationDemo.Services { // Service for handling phone verification. public class PhoneVerification : IPhoneVerification { private static ConcurrentDictionary<string, VerificationEntry> _verificationCodes = new(); public async Task<string> SendVerificationCodeAsync(string phoneNumber) { var code = new Random().Next(100000, 999999).ToString(); var expiry = DateTime.UtcNow.AddMinutes(5); _verificationCodes[phoneNumber] = new VerificationEntry { Code = code, Expiry = expiry, Attempts = 0 }; await Task.Delay(100); // Simulate SMS delay Console.WriteLine($"Verification code for {phoneNumber}: {code}"); return code; } public bool ValidateCode(string phoneNumber, string code) { if (_verificationCodes.TryGetValue(phoneNumber, out var entry)) { if (DateTime.UtcNow > entry.Expiry) { _verificationCodes.TryRemove(phoneNumber, out _); return false; // Code expired } if (entry.Attempts >= 5) { return false; // Too many attempts } if (entry.Code == code) { _verificationCodes.TryRemove(phoneNumber, out _); // Remove on success return true; } else { entry.Attempts++; return false; } } return false; } } public class VerificationEntry { public string Code { get; set; } = null!; public DateTime Expiry { get; set; } public int Attempts { get; set; } } }
Implementing Remote Validation
Remote Validation enables server-side validation from the client-side using AJAX, without requiring page reloads. We will implement Remote Validation for email uniqueness.
Create the RemoteValidationController
Create an empty MVC Controller named RemoteValidationController within the Controllers folder and then copy and paste the following code. The RemoteValidationController handles AJAX requests to validate email availability asynchronously, providing instant feedback without requiring page reloads.
using GmailRegistrationDemo.Data; using GmailRegistrationDemo.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; namespace GmailRegistrationDemo.Controllers { // Controller responsible for handling remote validation requests. public class RemoteValidationController : Controller { private readonly GmailDBContext _context; private readonly IGenerateEmailSuggestions _generateSuggestions; public RemoteValidationController(GmailDBContext context, IGenerateEmailSuggestions generateSuggestions) { _context = context; _generateSuggestions = generateSuggestions; } // Checks if the provided email is available. If not, returns suggestions. // Email: The email to validate // Returns a JSON result indicating availability or suggestions [AcceptVerbs("GET", "POST")] public async Task<IActionResult> IsEmailAvailable(string CustomEmail) { if (string.IsNullOrWhiteSpace(CustomEmail)) { return Json("Please enter a valid email address."); } var email = CustomEmail.Trim().ToLowerInvariant(); // Validate email format var emailAttribute = new EmailAddressAttribute(); if (!emailAttribute.IsValid(email)) { return Json("Please enter a valid email address."); } // Optional: Allowed domains whitelist example var allowedDomains = new List<string> { "gmail.com", "yahoo.com", "outlook.com", "example.com", "dotnettutorials.net" }; // Adjust as needed var domain = email.Split('@').Last(); if (!allowedDomains.Contains(domain)) { return Json($"Email domain '{domain}' is not allowed."); } // Check if email exists (case-insensitive) var emailExists = await _context.Users.AnyAsync(u => u.Email.ToLower() == email); if (emailExists) { // Optionally, provide alternative suggestions //var suggestedEmails = await _generateSuggestions.GenerateUniqueEmailsAsync(CustomEmail, 3); //var suggestions = string.Join(", ", suggestedEmails); //return Json($"This email address is already in use. Try one of these: {suggestions}"); return Json($"This email address is already in use."); } // If the email is available return Json(true); // Indicates success to jQuery Unobtrusive Validation } } }
Apply Remote Attributes in View Model
Please ensure that you apply the Remote attribute to the CustomEmail Property of the Step3ViewModel. Please modify the Step3ViewModel.cs class file as follows:
using Microsoft.AspNetCore.Mvc; using System.ComponentModel.DataAnnotations; namespace GmailRegistrationDemo.ViewModels { // ViewModel for Step 3: Choose Gmail Address. public class Step3ViewModel { [Required(ErrorMessage = "Gmail address is required.")] [EmailAddress(ErrorMessage = "Please enter a valid email address.")] public string Email { get; set; } = null!; // Initialize SuggestedEmails to prevent null references public List<string> SuggestedEmails { get; set; } = new List<string>(); // Selected Suggested Email public string? SuggestedEmail { get; set; } // Custom Email Input [EmailAddress(ErrorMessage = "Please enter a valid email address.")] [Remote(action: "IsEmailAvailable", controller: "RemoteValidation", ErrorMessage = "Email is already in use.")] public string? CustomEmail { get; set; } } }
Creating Registration Controller
Create an empty MVC Controller named RegistrationController within the Controllers folder and then copy and paste the following code. RegistrationController implements a multi-step registration flow, with each action method corresponding to a specific registration step (Steps 1 through 10). It manages form display, input validation, session persistence, email availability checking, password hashing (using BCrypt), phone verification, and final user creation.
using GmailRegistrationDemo.Data; using GmailRegistrationDemo.Models; using GmailRegistrationDemo.Services; using GmailRegistrationDemo.ViewModels; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace GmailRegistrationDemo.Controllers { public class RegistrationController : Controller { private readonly GmailDBContext _context; private readonly IGenerateEmailSuggestions _generateSuggestions; private readonly IPhoneVerification _phoneVerification; private readonly ILogger<RegistrationController> _logger; private readonly IRegistrationSessionService _registrationSessionService; public RegistrationController( GmailDBContext context, IGenerateEmailSuggestions generateSuggestions, IPhoneVerification phoneVerification, ILogger<RegistrationController> logger, IRegistrationSessionService registrationSessionService) { _context = context; _generateSuggestions = generateSuggestions; _phoneVerification = phoneVerification; _logger = logger; _registrationSessionService = registrationSessionService; } [HttpGet] public async Task<IActionResult> Step1() { try { var regId = Request.Cookies["RegistrationId"]; var regSession = await _registrationSessionService.GetOrCreateSessionAsync(regId); var model = new Step1ViewModel { FirstName = regSession.FirstName ?? string.Empty, LastName = regSession.LastName ?? string.Empty }; return View(model); } catch (Exception ex) { _logger.LogError(ex, "Error loading Step1."); return View("Error"); } } [HttpPost] public async Task<IActionResult> Step1(Step1ViewModel model) { try { if (ModelState.IsValid) { var regId = Request.Cookies["RegistrationId"]; var regSession = await _registrationSessionService.GetOrCreateSessionAsync(regId); regSession.FirstName = model.FirstName ?? string.Empty; regSession.LastName = model.LastName ?? string.Empty; regSession.LastUpdated = DateTime.UtcNow; await _registrationSessionService.SaveChangesAsync(); return RedirectToAction(nameof(Step2)); } return View(model); } catch (Exception ex) { _logger.LogError(ex, "Error processing Step1 POST."); return View("Error"); } } [HttpGet] public async Task<IActionResult> Step2() { try { var regId = Request.Cookies["RegistrationId"]; var regSession = await _registrationSessionService.GetOrCreateSessionAsync(regId); var model = new Step2ViewModel(); if (regSession.DateOfBirth.HasValue) { model.Month = regSession.DateOfBirth.Value.ToString("MMMM"); model.Day = regSession.DateOfBirth.Value.Day; model.Year = regSession.DateOfBirth.Value.Year; } if (regSession.Gender.HasValue) { model.Gender = regSession.Gender.Value; } return View(model); } catch (Exception ex) { _logger.LogError(ex, "Error loading Step2."); return View("Error"); } } [HttpPost] public async Task<IActionResult> Step2(Step2ViewModel model) { try { if (ModelState.IsValid) { // Validate Month (string) bool validMonth = DateTime.TryParseExact( model.Month, "MMMM", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime monthDate); if (!validMonth) { ModelState.AddModelError(nameof(model.Month), "Please select a valid month."); } else { int monthNumber = monthDate.Month; // Validate Year (non-nullable int) if (model.Year < 1900 || model.Year > DateTime.Now.Year) { ModelState.AddModelError(nameof(model.Year), "Please enter a valid year."); } // Validate Day based on month and year int daysInMonth = DateTime.DaysInMonth(model.Year, monthNumber); if (model.Day < 1 || model.Day > daysInMonth) { ModelState.AddModelError(nameof(model.Day), $"Please enter a valid day (1 to {daysInMonth})."); } // If no validation errors so far if (ModelState.ErrorCount == 0) { DateTime dob = new DateTime(model.Year, monthNumber, model.Day); // Check minimum age 13 var today = DateTime.Today; int age = today.Year - dob.Year; if (dob > today.AddYears(-age)) age--; if (age < 13) { ModelState.AddModelError(nameof(model.Year), "You must be at least 13 years old."); return View(model); } var regId = Request.Cookies["RegistrationId"]; var regSession = await _registrationSessionService.GetOrCreateSessionAsync(regId); regSession.DateOfBirth = dob; regSession.Gender = model.Gender; regSession.LastUpdated = DateTime.UtcNow; await _registrationSessionService.SaveChangesAsync(); return RedirectToAction(nameof(Step3)); } } } // Return view with validation messages return View(model); } catch (Exception ex) { _logger.LogError(ex, "Error processing Step2 POST."); return View("Error"); } } [HttpGet] public async Task<IActionResult> Step3() { try { var regId = Request.Cookies["RegistrationId"]; var regSession = await _registrationSessionService.GetOrCreateSessionAsync(regId); if (string.IsNullOrWhiteSpace(regSession.FirstName) || string.IsNullOrWhiteSpace(regSession.LastName)) return RedirectToAction(nameof(Step1)); var baseEmail = $"{regSession.FirstName.Trim().ToLowerInvariant()}.{regSession.LastName.Trim().ToLowerInvariant()}@example.com"; var suggestedEmails = await _generateSuggestions.GenerateUniqueEmailsAsync(baseEmail, 3); var model = new Step3ViewModel { SuggestedEmails = suggestedEmails, Email = (regSession.Email ?? baseEmail).Trim() }; if (suggestedEmails.Contains(model.Email)) model.SuggestedEmail = model.Email; else model.SuggestedEmail = "createOwn"; if (model.SuggestedEmail == "createOwn") model.CustomEmail = regSession.Email?.Trim(); return View(model); } catch (Exception ex) { _logger.LogError(ex, "Error loading Step3."); return View("Error"); } } [HttpPost] public async Task<IActionResult> Step3(Step3ViewModel model) { try { if (ModelState.IsValid) { var regId = Request.Cookies["RegistrationId"]; var regSession = await _registrationSessionService.GetOrCreateSessionAsync(regId); var normalizedEmail = model.Email?.Trim().ToLowerInvariant() ?? string.Empty; var exists = await _context.Users.AnyAsync(u => u.Email.ToLower() == normalizedEmail); if (exists) { ModelState.AddModelError("Email", "This email address is already taken. Please choose another."); model.SuggestedEmails = await _generateSuggestions.GenerateUniqueEmailsAsync($"{regSession.FirstName?.Trim().ToLowerInvariant()}.{regSession.LastName?.Trim().ToLowerInvariant()}@example.com", 3); return View(model); } regSession.Email = normalizedEmail; regSession.LastUpdated = DateTime.UtcNow; await _registrationSessionService.SaveChangesAsync(); return RedirectToAction(nameof(Step4)); } return View(model); } catch (Exception ex) { _logger.LogError(ex, "Error processing Step3 POST."); return View("Error"); } } [HttpGet] public IActionResult Step4() { try { return View(new Step4ViewModel()); } catch (Exception ex) { _logger.LogError(ex, "Error loading Step4."); return View("Error"); } } [HttpPost] public async Task<IActionResult> Step4(Step4ViewModel model) { try { if (ModelState.IsValid) { var regId = Request.Cookies["RegistrationId"]; var regSession = await _registrationSessionService.GetOrCreateSessionAsync(regId); regSession.PasswordHash = model.Password ?? string.Empty; // Will hash on final step regSession.LastUpdated = DateTime.UtcNow; await _registrationSessionService.SaveChangesAsync(); return RedirectToAction(nameof(Step5)); } return View(model); } catch (Exception ex) { _logger.LogError(ex, "Error processing Step4 POST."); return View("Error"); } } [HttpGet] public async Task<IActionResult> Step5() { try { var regId = Request.Cookies["RegistrationId"]; var regSession = await _registrationSessionService.GetOrCreateSessionAsync(regId); var model = new Step5ViewModel { CountryCode = regSession.CountryCode ?? string.Empty, PhoneNumber = regSession.PhoneNumber ?? string.Empty, CountryCodes = Step5ViewModel.GetCountryCodes() }; return View(model); } catch (Exception ex) { _logger.LogError(ex, "Error loading Step5."); return View("Error"); } } [HttpPost] public async Task<IActionResult> Step5(Step5ViewModel model) { try { if (ModelState.IsValid) { var regId = Request.Cookies["RegistrationId"]; var regSession = await _registrationSessionService.GetOrCreateSessionAsync(regId); regSession.CountryCode = model.CountryCode ?? string.Empty; regSession.PhoneNumber = model.PhoneNumber ?? string.Empty; regSession.LastUpdated = DateTime.UtcNow; await _registrationSessionService.SaveChangesAsync(); await _phoneVerification.SendVerificationCodeAsync($"{model.CountryCode}{model.PhoneNumber}"); return RedirectToAction(nameof(Step6)); } model.CountryCodes = Step5ViewModel.GetCountryCodes(); return View(model); } catch (Exception ex) { _logger.LogError(ex, "Error processing Step5 POST."); return View("Error"); } } [HttpGet] public IActionResult Step6() { try { return View(new Step6ViewModel()); } catch (Exception ex) { _logger.LogError(ex, "Error loading Step6."); return View("Error"); } } [HttpPost] public async Task<IActionResult> Step6(Step6ViewModel model) { try { if (ModelState.IsValid) { var regId = Request.Cookies["RegistrationId"]; var regSession = await _registrationSessionService.GetOrCreateSessionAsync(regId); var phone = $"{regSession.CountryCode}{regSession.PhoneNumber}"; if (_phoneVerification.ValidateCode(phone, model.VerificationCode ?? string.Empty)) { return RedirectToAction(nameof(Step7)); } else { ModelState.AddModelError("VerificationCode", "Invalid verification code."); } } return View(model); } catch (Exception ex) { _logger.LogError(ex, "Error processing Step6 POST."); return View("Error"); } } [HttpGet] public async Task<IActionResult> Step7() { try { var regId = Request.Cookies["RegistrationId"]; var regSession = await _registrationSessionService.GetOrCreateSessionAsync(regId); var model = new Step7ViewModel { RecoveryEmail = regSession.RecoveryEmail ?? string.Empty }; return View(model); } catch (Exception ex) { _logger.LogError(ex, "Error loading Step7."); return View("Error"); } } [HttpPost] public async Task<IActionResult> Step7(Step7ViewModel model) { try { if (ModelState.IsValid) { var regId = Request.Cookies["RegistrationId"]; var regSession = await _registrationSessionService.GetOrCreateSessionAsync(regId); regSession.RecoveryEmail = model.RecoveryEmail ?? string.Empty; regSession.LastUpdated = DateTime.UtcNow; await _registrationSessionService.SaveChangesAsync(); return RedirectToAction(nameof(Step8)); } return View(model); } catch (Exception ex) { _logger.LogError(ex, "Error processing Step7 POST."); return View("Error"); } } [HttpGet] public async Task<IActionResult> Step8() { try { var regId = Request.Cookies["RegistrationId"]; var regSession = await _registrationSessionService.GetOrCreateSessionAsync(regId); var model = new Step8ViewModel { FullName = $"{regSession.FirstName ?? string.Empty} {regSession.LastName ?? string.Empty}".Trim(), Email = regSession.Email ?? string.Empty, PhoneNumber = $"{regSession.CountryCode ?? string.Empty}{regSession.PhoneNumber ?? string.Empty}", Gender = regSession.Gender?.ToString() ?? string.Empty, RecoveryEmail = regSession.RecoveryEmail ?? string.Empty, DateOfBirth = regSession.DateOfBirth ?? DateTime.MinValue }; return View(model); } catch (Exception ex) { _logger.LogError(ex, "Error loading Step8."); return View("Error"); } } [HttpPost] public IActionResult Step8Confirm() { try { return RedirectToAction(nameof(Step9)); } catch (Exception ex) { _logger.LogError(ex, "Error processing Step8 POST."); return View("Error"); } } [HttpGet] public IActionResult Step9() { try { return View(new Step9ViewModel()); } catch (Exception ex) { _logger.LogError(ex, "Error loading Step9."); return View("Error"); } } [HttpPost] public IActionResult Step9(Step9ViewModel model) { try { if (ModelState.IsValid && model.Agree) { return RedirectToAction(nameof(Step10)); } ModelState.AddModelError("Agree", "You must agree to the terms to proceed."); return View(model); } catch (Exception ex) { _logger.LogError(ex, "Error processing Step9 POST."); return View("Error"); } } [HttpGet] public async Task<IActionResult> Step10() { try { var regId = Request.Cookies["RegistrationId"]; var regSession = await _registrationSessionService.GetOrCreateSessionAsync(regId); if (string.IsNullOrWhiteSpace(regSession.FirstName) || string.IsNullOrWhiteSpace(regSession.LastName) || string.IsNullOrWhiteSpace(regSession.Email) || string.IsNullOrWhiteSpace(regSession.PasswordHash) || !regSession.DateOfBirth.HasValue || !regSession.Gender.HasValue || string.IsNullOrWhiteSpace(regSession.CountryCode) || string.IsNullOrWhiteSpace(regSession.PhoneNumber)) { return RedirectToAction(nameof(Step1)); } var user = new User { FirstName = regSession.FirstName, LastName = regSession.LastName, Email = regSession.Email, DateOfBirth = regSession.DateOfBirth.Value, Gender = regSession.Gender.Value, CountryCode = regSession.CountryCode, PhoneNumber = regSession.PhoneNumber, RecoveryEmail = regSession.RecoveryEmail ?? string.Empty }; // Hash the password using BCrypt user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(regSession.PasswordHash); _context.Users.Add(user); await _context.SaveChangesAsync(); await _registrationSessionService.DeleteSessionAsync(regSession); return View("Dashboard", user); } catch (Exception ex) { _logger.LogError(ex, "Error processing Step10."); return View("Error"); } } public async Task<IActionResult> Dashboard() { int UserId = 1; var user = await _context.Users.FindAsync(UserId); return View(user); } } }
Modify the appsettings.json file.
It contains configuration settings, including the connection string for the SQL Server database. This connection string is used by Entity Framework Core to connect to the database named GmailRegistrationDB. So, please modify the appsettings.json file as follows:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "EFCoreDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=GmailRegistrationDB;Trusted_Connection=True;TrustServerCertificate=True;" } }
Configure Services, DbContext, and Session in Program Class:
This is the application startup file where services, middleware, and the HTTP request pipeline are configured. It registers Entity Framework Core with SQL Server, session state management, and dependency injection for custom services, such as email suggestion generation, phone verification, and registration session handling. It also sets up routing and static files. Please modify the Program class as follows.
using GmailRegistrationDemo.Data; using GmailRegistrationDemo.Services; using Microsoft.EntityFrameworkCore; namespace GmailRegistrationDemo { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); //Configure the ConnectionString and DbContext class builder.Services.AddDbContext<GmailDBContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreDBConnection")); }); // Add Session services builder.Services.AddDistributedMemoryCache(); builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(30); // Set appropriate timeout options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; }); // Register Services builder.Services.AddScoped<IGenerateEmailSuggestions, GenerateEmailSuggestions>(); builder.Services.AddSingleton<IPhoneVerification, PhoneVerification>(); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped<IRegistrationSessionService, RegistrationSessionService>(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseSession(); // Enable Session app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Registration}/{action=Step1}/{id?}"); app.Run(); } } }
Generating and Applying Database Migration
Next, we need to generate the Migration and execute it so that it creates and updates the database schema. So, open Package Manager Console and execute the Add-Migration and Update-Database commands as follows.
With this, the GmailRegistrationDB Database with required tables should have been created as shown in the image below:
Designing the Views
We will create separate views for each registration step. First, create a folder named Registration inside the Views folder where we will create all our cshtml files:
Add a Progress Bar
Create a partial view named _ProgressBar.cshtml within the Views/Shared folder, and copy and paste the following code. This reusable partial view displays a visual step indicator at the top of each registration step page, showing the current step, completed steps with check marks, and the total number of steps.
@{ var currentStep = ViewBag.CurrentStep ?? 1; var totalSteps = 9; var steps = new[] { "Name", "DOB & Gender", "Gmail", "Password", "Phone", "OTP", "Recovery", "Review", "Terms" }; } <div class="mb-4"> <div class="row text-center mb-2"> @for (var i = 0; i < totalSteps; i++) { var stepNum = i + 1; var isActive = stepNum == currentStep; var isCompleted = stepNum < currentStep; <div class="col px-0"> <div class="rounded-circle mx-auto d-flex align-items-center justify-content-center" style="width:36px;height:36px; background:@(isCompleted ? "#198754" : isActive ? "#0d6efd" : "#dee2e6"); color:@(isCompleted || isActive ? "#fff" : "#6c757d"); font-weight:600;"> @(isCompleted ? "✓" : stepNum.ToString()) </div> <div class="small mt-1" style="white-space:nowrap;"> @steps[i] </div> </div> } </div> <div class="progress" style="height: 6px;"> <div class="progress-bar" role="progressbar" style="width:@(((double)currentStep - 1) / (totalSteps - 1) * 100)%;"> </div> </div> </div>
Step1.cshtml: Enter First and Last Name View
Create a view file named Step1.cshtml inside the Views/Registration folder, and then copy and paste the following code. This View displays a form to collect the user’s first and last names.
@model GmailRegistrationDemo.ViewModels.Step1ViewModel @{ ViewData["Title"] = "Step 1: Enter Your Name"; ViewBag.CurrentStep = 1; } <h2 class="mb-4 text-center">@ViewData["Title"]</h2> @await Html.PartialAsync("_ProgressBar") <div class="card shadow-sm"> <div class="card-body"> <form asp-action="Step1" asp-controller="Registration" method="post"> <div class="form-group mt-3"> <label asp-for="FirstName"></label> <input asp-for="FirstName" class="form-control" placeholder="First Name" /> <span asp-validation-for="FirstName" class="text-danger"></span> </div> <div class="form-group mt-3"> <label asp-for="LastName"></label> <input asp-for="LastName" class="form-control" placeholder="Last Name" /> <span asp-validation-for="LastName" class="text-danger"></span> </div> <div class="form-group mt-3 text-end"> <button type="submit" class="btn btn-primary">Next</button> </div> </form> </div> </div> @section Scripts { @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } }
Step2.cshtml: Enter Date of Birth and Gender View
Create a view file named Step2.cshtml inside the Views/Registration folder, and then copy and paste the following code. This View provides inputs for the user’s date of birth and gender selection.
@model GmailRegistrationDemo.ViewModels.Step2ViewModel @{ ViewData["Title"] = "Step 2: Enter Your Date of Birth and Gender"; ViewBag.CurrentStep = 2; } <h2 class="mb-4 text-center">@ViewData["Title"]</h2> @await Html.PartialAsync("_ProgressBar") <div class="card shadow-sm"> <div class="card-body"> <form asp-action="Step2" asp-controller="Registration" method="post"> <!-- Row for Month, Day, Year --> <div class="row mb-4"> <!-- Month Field --> <div class="form-group col-md-4"> <label asp-for="Month" class="form-label"></label> <select asp-for="Month" class="form-select"> <option value="">Select Month</option> @foreach (var month in System.Globalization.DateTimeFormatInfo.InvariantInfo.MonthNames.Take(12)) { <option value="@month">@month</option> } </select> <span asp-validation-for="Month" class="text-danger"></span> </div> <!-- Day Field --> <div class="form-group col-md-4"> <label asp-for="Day" class="form-label"></label> <input asp-for="Day" class="form-control" placeholder="Day" type="number" min="1" max="31" oninput="if(this.value.length > 2) this.value = this.value.slice(0,2)" /> <span asp-validation-for="Day" class="text-danger"></span> </div> <!-- Year Field --> <div class="form-group col-md-4"> <label asp-for="Year" class="form-label"></label> <input asp-for="Year" class="form-control" placeholder="Year" type="number" min="1900" max="2100" oninput="if(this.value.length > 4) this.value = this.value.slice(0,4)" /> <span asp-validation-for="Year" class="text-danger"></span> </div> </div> <!-- End of Row for Month, Day, Year --> <!-- Gender Field --> <div class="form-group mb-4"> <label asp-for="Gender" class="form-label"></label> <select asp-for="Gender" class="form-select"> <option value="">Select Gender</option> @foreach (var gender in Enum.GetValues(typeof(Gender)).Cast<Gender>()) { <option value="@gender">@gender</option> } </select> <span asp-validation-for="Gender" class="text-danger"></span> </div> <!-- Back and Next Buttons --> <div class="form-group text-end"> <a asp-action="Step1" class="btn btn-secondary">Back</a> <button type="submit" class="btn btn-primary px-4">Next</button> </div> </form> </div> </div> @section Scripts { @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } }
Step3.cshtml: Choose Gmail Address View
Create a view file named Step3.cshtml inside the Views/Registration folder, and then copy and paste the following code. This View allows the user to enter and validate their desired Gmail address.
@model GmailRegistrationDemo.ViewModels.Step3ViewModel @{ ViewData["Title"] = "Step 3: Choose Your Gmail Address"; ViewBag.CurrentStep = 3; // Determine whether to display the custom email input based on the selected option var displayStyle = Model.SuggestedEmail == "createOwn" ? "block" : "none"; } <h2 class="mb-4 text-center">@ViewData["Title"]</h2> @await Html.PartialAsync("_ProgressBar") <div class="card shadow-sm"> <div class="card-body"> <!-- Form to select or create a Gmail address --> <form asp-action="Step3" asp-controller="Registration" method="post"> <!-- Email Selection Section --> <div class="form-group mb-4"> <label class="form-label">Pick a Gmail address or create your own:</label> <!-- Suggested Emails as Radio Buttons --> <div class="form-check"> @foreach (var email in Model.SuggestedEmails) { if (Model.SuggestedEmail == email) { <input class="form-check-input" type="radio" name="SuggestedEmail" id="email_@email" value="@email" checked="checked" /> } else { <input class="form-check-input" type="radio" name="SuggestedEmail" id="email_@email" value="@email" /> } <label class="form-check-label" for="email_@email"> @email </label> <br /> } </div> <!-- Option to Create Own Email --> <div class="form-check mt-2"> @if (Model.SuggestedEmail == "createOwn") { <input class="form-check-input" type="radio" name="SuggestedEmail" id="createOwn" value="createOwn" checked> } else { <input class="form-check-input" type="radio" name="SuggestedEmail" id="createOwn" value="createOwn"> } <label class="form-check-label" for="createOwn"> Create your own Gmail address </label> </div> <!-- Custom Email Input Field --> <div class="mt-3" id="customEmailDiv" style="display:@displayStyle;"> <label for="CustomEmail" class="form-label">Your Custom Gmail Address:</label> <input id="CustomEmail" asp-for="CustomEmail" class="form-control" placeholder="Enter your desired Gmail address" type="email" /> <span asp-validation-for="CustomEmail" class="text-danger"></span> </div> <!-- Hidden Field to Store the Final Email Selection --> <input type="hidden" id="Email" name="Email" value="@Model.Email" /> <span asp-validation-for="Email" class="text-danger"></span> </div> <!-- Navigation Buttons --> <div class="form-group mt-3 text-end"> <a asp-action="Step2" class="btn btn-secondary">Back</a> <button type="submit" class="btn btn-primary">Next</button> </div> </form> </div> </div> @section Scripts { @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } <script> $(document).ready(function () { function updateEmailField() { var selectedValue = $('input[name="SuggestedEmail"]:checked').val(); if (selectedValue === 'createOwn') { var customEmail = $('#CustomEmail').val(); $('#Email').val(customEmail); } else { $('#Email').val(selectedValue); } } // Show or hide the custom email input on load if ($('input[name="SuggestedEmail"]:checked').val() === 'createOwn') { $('#customEmailDiv').show(); $('#CustomEmail').attr('required', true); } else { $('#customEmailDiv').hide(); $('#CustomEmail').removeAttr('required'); $('#CustomEmail').val(''); } // Handle radio button changes $('input[name="SuggestedEmail"]').change(function () { if ($('#createOwn').is(':checked')) { $('#customEmailDiv').show(); $('#CustomEmail').attr('required', true); } else { $('#customEmailDiv').hide(); $('#CustomEmail').removeAttr('required'); $('#CustomEmail').val(''); } updateEmailField(); }); // Update hidden input when custom email changes $('#CustomEmail').on('input', function () { updateEmailField(); }); // Update hidden email field before form submit $('form').on('submit', function () { updateEmailField(); }); }); </script> }
Step4.cshtml: Create a Strong Password View
Create a view file named Step4.cshtml inside the Views/Registration folder, and then copy and paste the following code. This View enables the user to create and confirm a strong password, with an option to show/hide the password.
@model GmailRegistrationDemo.ViewModels.Step4ViewModel @{ ViewData["Title"] = "Step 4: Create a Strong Password"; ViewBag.CurrentStep = 4; } <h2 class="mb-4 text-center">@ViewData["Title"]</h2> @await Html.PartialAsync("_ProgressBar") <div class="card shadow-sm"> <div class="card-body"> <form asp-action="Step4" asp-controller="Registration" method="post"> <div class="form-group mt-3"> <label asp-for="Password"></label> <input asp-for="Password" class="form-control" placeholder="Enter Password" /> <span asp-validation-for="Password" class="text-danger"></span> </div> <div class="form-group mt-3"> <label asp-for="ConfirmPassword"></label> <input asp-for="ConfirmPassword" class="form-control" placeholder="Enter Confirm Password" /> <span asp-validation-for="ConfirmPassword" class="text-danger"></span> </div> <div class="form-group form-check mt-3"> <input asp-for="ShowPassword" id="showPassword" class="form-check-input" type="checkbox" /> <label asp-for="ShowPassword" class="form-check-label">Show Password</label> </div> <!-- Back and Next Buttons --> <div class="form-group mt-3 text-end"> <a asp-action="Step3" class="btn btn-secondary">Back</a> <button type="submit" class="btn btn-primary">Next</button> </div> </form> </div> </div> @section Scripts { @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } <script> $(document).ready(function () { // Toggle password visibility $('#showPassword').change(function () { const type = $(this).is(':checked') ? 'text' : 'password'; $('input[name="Password"]').attr('type', type); $('input[name="ConfirmPassword"]').attr('type', type); }); }); </script> }
Step5.cshtml: Enter Phone Number View
Create a view file named Step5.cshtml inside the Views/Registration folder, and then copy and paste the following code. This View collects the user’s phone number and informs them about SMS verification.
@model GmailRegistrationDemo.ViewModels.Step5ViewModel @{ ViewData["Title"] = "Step 5: Enter Your Phone Number"; ViewBag.CurrentStep = 5; } <h2 class="mb-4 text-center">@ViewData["Title"]</h2> @await Html.PartialAsync("_ProgressBar") <div class="card shadow-sm"> <div class="card-body"> <p>Google will verify this number via SMS (charges may apply).</p> <form asp-action="Step5" asp-controller="Registration" method="post"> <div class="form-group row"> <!-- Country Code --> <div class="col-md-4"> <label asp-for="CountryCode"></label> <select asp-for="CountryCode" asp-items="Model.CountryCodes" class="form-select"> <option value="">Select Country</option> </select> <span asp-validation-for="CountryCode" class="text-danger"></span> </div> <!-- Phone Number --> <div class="col-md-8"> <label asp-for="PhoneNumber"></label> <input asp-for="PhoneNumber" class="form-control" placeholder="Enter your phone number" /> <span asp-validation-for="PhoneNumber" class="text-danger"></span> </div> </div> <!-- Back and Next Buttons --> <div class="form-group mt-3 text-end"> <a asp-action="Step4" class="btn btn-secondary">Back</a> <button type="submit" class="btn btn-primary">Next</button> </div> </form> </div> </div> @section Scripts { @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } }
Step6.cshtml: Phone Verification Code View
Create a view file named Step6.cshtml inside the Views/Registration folder, and then copy and paste the following code. This View provides a field for the user to enter the received verification code.
@model GmailRegistrationDemo.ViewModels.Step6ViewModel @{ ViewData["Title"] = "Step 6: Enter Verification Code"; ViewBag.CurrentStep = 6; } <h2 class="mb-4 text-center">@ViewData["Title"]</h2> @await Html.PartialAsync("_ProgressBar") <div class="card shadow-sm"> <div class="card-body"> <p>Enter the 6-digit verification code sent to your phone.</p> <form asp-action="Step6" asp-controller="Registration" method="post"> <div class="form-group"> <label asp-for="VerificationCode"></label> <input asp-for="VerificationCode" maxlength="6" class="form-control" placeholder="Enter verification code" /> <span asp-validation-for="VerificationCode" class="text-danger"></span> </div> <!-- Back and Next Buttons --> <div class="form-group mt-3 text-end"> <a asp-action="Step5" asp-controller="Registration" class="btn btn-secondary">Back</a> <button type="submit" class="btn btn-primary">Verify</button> </div> </form> </div> </div> @section Scripts { @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } }
Step7.cshtml: Add Recovery Email View
Create a view file named Step7.cshtml inside the Views/Registration folder, and then copy and paste the following code. This View optionally allows the user to add a recovery email address for account security. Users can skip adding a recovery email by clicking the Skip button, which redirects to Step 8.
@model GmailRegistrationDemo.ViewModels.Step7ViewModel @{ ViewData["Title"] = "Step 7: Add Recovery Email"; ViewBag.CurrentStep = 7; } <h2 class="mb-4 text-center">@ViewData["Title"]</h2> @await Html.PartialAsync("_ProgressBar") <div class="card shadow-sm"> <div class="card-body"> <p>The address where Google can contact you if there’s unusual activity in your account or if you get locked out.</p> <form asp-action="Step7" asp-controller="Registration" method="post"> <div class="form-group mt-3"> <label asp-for="RecoveryEmail"></label> <input asp-for="RecoveryEmail" class="form-control" placeholder="Enter recovery email (optional)" /> <span asp-validation-for="RecoveryEmail" class="text-danger"></span> </div> <!-- Back, Next and Skip button Buttons --> <div class="form-group mt-3 text-end"> <a asp-action="Step6" asp-controller="Registration" class="btn btn-secondary">Back</a> <button type="submit" class="btn btn-primary">Next</button> <a asp-action="Step8" asp-controller="Registration" class="btn btn-secondary">Skip</a> </div> </form> </div> </div> @section Scripts { @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } }
Step8.cshtml: Review Account Information
Create a view file named Step8.cshtml inside the Views/Registration folder, and then copy and paste the following code. This View presents a summary of all entered information for the user to review before finalizing registration.
@model GmailRegistrationDemo.ViewModels.Step8ViewModel @{ ViewData["Title"] = "Step 8: Review Your Account Info"; ViewBag.CurrentStep = 8; } <h2 class="mb-4 text-center">@ViewData["Title"]</h2> @await Html.PartialAsync("_ProgressBar") <div class="card shadow-sm"> <div class="card-header bg-primary text-white fw-semibold fs-5"> Please review your account information </div> <div class="card-body"> <form asp-action="Step8Confirm" asp-controller="Registration" method="post" novalidate> <dl class="row mb-4"> <dt class="col-sm-4 fw-semibold">Full Name:</dt> <dd class="col-sm-8">@Model.FullName</dd> <dt class="col-sm-4 fw-semibold">Gmail Address:</dt> <dd class="col-sm-8">@Model.Email</dd> <dt class="col-sm-4 fw-semibold">Phone Number:</dt> <dd class="col-sm-8">@Model.PhoneNumber</dd> <dt class="col-sm-4 fw-semibold">Gender:</dt> <dd class="col-sm-8">@Model.Gender</dd> <dt class="col-sm-4 fw-semibold">Date Of Birth:</dt> <dd class="col-sm-8">@Model.DateOfBirth.ToString("MMMM dd, yyyy")</dd> @if (!string.IsNullOrWhiteSpace(Model.RecoveryEmail)) { <dt class="col-sm-4 fw-semibold">Recovery Email:</dt> <dd class="col-sm-8">@Model.RecoveryEmail</dd> } </dl> <!-- Back, Next and Skip button Buttons --> <div class="form-group mt-1 text-end"> <a asp-action="Step7" asp-controller="Registration" class="btn btn-secondary">Back</a> <button type="submit" class="btn btn-primary">Confirm and Continue</button> </div> </form> </div> </div>
Step9.cshtml: Privacy and Terms
Create a view file named Step9.cshtml inside the Views/Registration folder, and then copy and paste the following code. This View displays the Privacy Policy and Terms of Service, requiring the user to agree before proceeding. Users must agree to the Privacy Policy and Terms of Service to proceed.
@model GmailRegistrationDemo.ViewModels.Step9ViewModel @{ ViewData["Title"] = "Step 9: Privacy and Terms"; ViewBag.CurrentStep = 9; } <h2 class="mb-4 text-center">@ViewData["Title"]</h2> @await Html.PartialAsync("_ProgressBar") <div class="card shadow-sm"> <div class="card-body"> <div class="privacy-terms"> <p>To create a Google Account, you’ll need to agree to the Terms of Service below.</p> <p>[Insert Privacy Policy and Terms of Service Content Here]</p> </div> <form asp-action="Step9" asp-controller="Registration" method="post"> <div class="form-group form-check"> <input asp-for="Agree" id="agreeCheckbox" class="form-check-input" type="checkbox" /> <label asp-for="Agree" class="form-check-label"></label> <span asp-validation-for="Agree" class="text-danger"></span> </div> <div class="form-group mt-3 text-end"> <button type="submit" id="agreeButton" disabled class="btn btn-primary">I Agree</button> </div> </form> </div> </div> @section Scripts { @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } <script> $(document).ready(function () { // Toggle button state based on checkbox $('#agreeCheckbox').change(function () { if ($(this).is(':checked')) { $('#agreeButton').prop('disabled', false); } else { $('#agreeButton').prop('disabled', true); } }); }); </script> }
Dashboard.cshtml: Registration Success and Dashboard
Create a view file named Dashboard.cshtml inside the Views/Registration folder and then copy and paste the following code. This View confirms successful registration and displays the user’s account details on a dashboard.
@model GmailRegistrationDemo.Models.User @{ ViewData["Title"] = "Dashboard"; } <div class="card shadow-sm"> <div class="card-body"> <div class="text-center mb-4"> <h3 class="mb-2">Welcome, @Model.FirstName!</h3> <p class="text-success">Your Gmail account has been created successfully.</p> </div> <h4 class="mb-3">Your Account Details:</h4> <ul class="list-group mb-4"> <li class="list-group-item d-flex justify-content-between align-items-center"> <strong>Full Name:</strong> <span>@Model.FirstName @Model.LastName</span> </li> <li class="list-group-item d-flex justify-content-between align-items-center"> <strong>Email:</strong> <span>@Model.Email</span> </li> <li class="list-group-item d-flex justify-content-between align-items-center"> <strong>Phone Number:</strong> <span>@Model.CountryCode@Model.PhoneNumber</span> </li> @if (!string.IsNullOrEmpty(Model.RecoveryEmail)) { <li class="list-group-item d-flex justify-content-between align-items-center"> <strong>Recovery Email:</strong> <span>@Model.RecoveryEmail</span> </li> } <li class="list-group-item d-flex justify-content-between align-items-center"> <strong>Date of Birth:</strong> <span>@Model.DateOfBirth.ToString("MMMM dd, yyyy")</span> </li> <li class="list-group-item d-flex justify-content-between align-items-center"> <strong>Gender:</strong> <span>@Model.Gender</span> </li> </ul> </div> </div>
Modifying the _Layout.cshtml file:
Finally, modify the _Layout.cshtml file as follows. The Layout view defines the overall HTML structure and shared UI for all pages: includes the Bootstrap and icons CDN, a sticky-top navigation bar, a responsive main content container, and a professional footer. The layout also includes section placeholders for custom page scripts.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - GmailRegistrationDemo</title> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> <link rel="stylesheet" href="~/GmailRegistrationDemo.styles.css" asp-append-version="true" /> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet" /> </head> <body class="d-flex flex-column min-vh-100 bg-light"> <!-- Header --> <header> <nav class="navbar navbar-expand-lg navbar-dark bg-primary shadow-sm sticky-top"> <div class="container"> <a class="navbar-brand fw-bold" href="/"> <i class="bi bi-envelope-at-fill me-2"></i>Gmail Registration </a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav ms-auto"> <li class="nav-item"> <a class="nav-link" asp-controller="Home" asp-action="Index">Home</a> </li> <li class="nav-item"> <a class="nav-link" asp-controller="Registration" asp-action="Step1">Register</a> </li> <li class="nav-item"> <a class="nav-link" asp-controller="Registration" asp-action="Dashboard">Dashboard</a> </li> </ul> </div> </div> </nav> </header> <!-- Main Content --> <main class="container py-2 flex-grow-1"> <div class="row justify-content-center"> <div class="col-12 col-md-10 col-lg-8"> @RenderBody() </div> </div> </main> <!-- Footer --> <footer class="bg-dark text-light pt-4 mt-5 border-top"> <div class="container"> <div class="row align-items-center pb-3"> <div class="col-md-6 text-center text-md-start mb-2 mb-md-0"> <span class="fw-semibold">GmailRegistrationDemo</span> © @DateTime.Now.Year </div> <div class="col-md-6 text-center text-md-end"> <a asp-controller="Home" asp-action="Privacy" class="text-decoration-none text-light me-3">Privacy Policy</a> <a href="mailto:support@example.com" class="text-decoration-none text-light me-3"><i class="bi bi-envelope-fill"></i></a> <a href="https://github.com/" target="_blank" class="text-decoration-none text-light"><i class="bi bi-github"></i></a> </div> </div> </div> </footer> <!-- Scripts --> <script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/js/site.js" asp-append-version="true"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> @await RenderSectionAsync("Scripts", required: false) </body> </html>
That’s it. We have completed our Gmail-like registration System using ASP.NET Core MVC and Entity Framework Core. Now, run the application and test the functionalities; it should work as expected.Â
In the next article, I will discuss the development of a Blog Management Application using ASP.NET Core MVC and Entity Framework Core. I hope you enjoy this in-depth article on a Gmail-like registration system using ASP.NET Core MVC and Entity Framework Core. I hope this gives you a clear understanding of how the Gmail Registration System was developed and implemented using ASP.NET Core MVC and EF Core.
🎥 Watch Our Step-by-Step Tutorial: Gmail-Like Registration System using ASP.NET Core MVC!
Looking to build a smooth, multi-step user registration system just like Gmail?
Our detailed video guide walks you through everything — from creating user-friendly forms, implementing email validation, securing passwords, to phone number verification with OTP.
👉 Click here to watch now: https://youtu.be/X7EMjJQaUFM
Whether you’re a beginner or an experienced developer, this tutorial will help you build a secure and scalable registration workflow for your ASP.NET Core MVC projects!
Happy coding!