Skip to content
This repository was archived by the owner on Feb 20, 2021. It is now read-only.

Files

Latest commit

author
Terry Fei (Pactera)
Nov 15, 2019
9c4b562 · Nov 15, 2019

History

History

ASP.NET MVC4 Workflow Enabled User Registration

ASP.NET MVC4 Workflow Enabled User Registration

Requires

  • Visual Studio 2012

License

  • Apache License, Version 2.0

Technologies

  • Windows Workflow
  • WF4
  • ASP.NET MVC 4

Topics

  • Workflow

Updated

  • 05/07/2012

Description

One thing that many web sites do is to verify email addresses by sending you an email to complete registration.  I decided to build a Registration system for ASP.NET MVC using Windows Workflow Foundation.
When you create a new ASP.NET MVC web site, the site comes with a simple account controller that integrates with ASP.NET Membership.  It provides basic one step registration and log-in support.  I wanted to take this much farther and provide a simple self-contained registration verification system.

Scenarios

When I plan work like this, my first step is to prepare the list of scenarios I'm working on so I don't get distracted and don't miss anything important
Given When Then
A user registers for the site with a valid email address
The user clicks on Register
  • The user is added to the Membership database with the isApproved flag set to false
  • A Workflow is started to manage the membership verification
  • The workflow sends an email to the members email address
A user receives the email verification message
The user clicks the link in the verification email
  • A browser launches and opens the Verification page providing a verificationCode in the URL query string
  • The Workflow is loaded and resumed with the confirmation command
  • The membership is approved
A user attempts to log-in after registration but before the verification email is confirmed The user clicks on log in
  • The Membership is found in the membership database
  • Because the isApproved flag is false, an error is generated including a link to the page to re-send the verification email
A user cannot find the verification email and wants it sent again. The user navigates to the site, enters username and password and clicks on log in then clicks on the link in the error message to navigate to the re-send confirmation page The user clicks re-send to send the message again
  • The Workflow instanceId for the email is located using a Promoted Property. If not found, an error is displayed for an invalid email address
  • The Workflow is loaded and the send mail command is resumed
After registration, the user fails to click on the link in the confirmation mail The timeout interval expires
  • The Workflow with an expired timer is detected and the workflow is loaded
  • The timeout action increments a counter and a second, third or fourth message can be sent to the user
  • If the timeout has exceeded the maximum number of timeouts, the user account is deleted
After registration, the user decides to cancel registration The user clicks the cancel link in the email
  • The Workflow is loaded
  • The workflow is resumed with the cancel command
  • The user account is deleted

Implementation

For my platform, I chose Visual Studio 11, .NET 4.5 and ASP.NET MVC 4.  However the same concepts will work fine with .NET 4.0 and MVC 3 given a few minor modifications.

Step 1 - Creating users with isApproved False

For this step I simply searched account controller for isApproved. I found that by default when users are created, isApproved is set to true.  In MVC 4 there are two methods that create users they are Register and JsonRegister. The modification is shown below.
C#
Edit|Remove
csharp
/// <summary> 
/// Provides registration support for the registration pop-up dialog 
/// </summary> 
/// <param name="model">The model</param> 
/// <returns>An action result</returns> 
/// <remarks> 
/// The default implementation of this method creates and automatically approves users.  In this case we don't want to approve a user until their email is verified.   
/// The default implementation also implicitly logs in the created user.  In this case we do not want to log in the newly created user. 
/// </remarks> 
[AllowAnonymous] 
[HttpPost] 
public ActionResult JsonRegister(RegisterModel model) 
{ 
    if (this.ModelState.IsValid) 
    { 
        // Attempt to register the user 
        MembershipCreateStatus createStatus; 
 
        // TODO: Notice how we set isApproved = false until email verification is complete 

        Membership.CreateUser(              model.UserName,              model.Password,              model.Email,              passwordQuestion: null,              passwordAnswer: null,              isApproved: false,              providerUserKey: null,              status: out createStatus);            if (createStatus == MembershipCreateStatus.Success)          {              // TODO: Notice how we do not log in here but start the verification process              this.VerifyRegistration(model);                // TODO: Notice how we redirect to the confirmation page              return this.Json(new { success = true, redirect = this.Url.Action("Confirmation") });          }          this.ModelState.AddModelError("", ErrorCodeToString(createStatus));      }        // If we got this far, something failed      return this.Json(new { errors = this.GetErrorsFromModelState() });  } 

Step 2: Sending an Email

For this step I’m going to need an activity that can send email and I want to supply a nicely formatted HTML email with the username embedded and an absolute URL to the Site.css stylesheet.  For this example, I decided to create a SendMail activity that uses file based email templates.  This allows me to treat the body of the HTML mail as content from the site perspective.  To improve performance I cache the HTML files after they are read and check to see if the source file has changed before using a cached copy.
Using SmtpClient with AsyncCodeActivity was a particular challenge because the SmtpClient class uses an event based async model (EAP) and it took me a while to work out how to use a TaskCompletionSource with AsyncCodeActivity.  Take a look at the SendMail.cs file for more details.
In the body of the email, I will have to include an absolute URL to the verification page including a verificationCode which is simply the InstanceId of the workflow.  Given the enormous amount of data that can apply to an email message, I decided to create a type to pass between the MVC code and the Workflow which contains everything I need.
Problem: HTML Email requires fully qualified URLs
I want the HTML email to have links which must be fully qualified. I need links to the Site.css file so I can take advantage of styling in the email and the verification URL.  To do this, I created the some extension methods to the UrlHelper class
C#
Edit|Remove
csharp
public static string FullyQualifiedAction(this UrlHelper urlHelper, string actionName, string controllerName) 
{ 
    return FullyQualify( 
        urlHelper.RequestContext.HttpContext.Request.Url, urlHelper.Action(actionName, controllerName)); 
}
Now when I want to get the fully qualified URL it is very simple
C#
Edit|Remove
csharp
// Created extension methods to provide fully qualified URLs for email 
VerificationUrl = this.Url.FullyQualifiedAction("Verification"), 
CancelUrl = this.Url.FullyQualifiedAction("Cancel"), 
StylesUrl = this.Url.FullyQualifiedContent("~/Content/Site.css"), 
Problem: How to merge arguments into the HTML email
In the HTML email I want to merge two kinds of arguments.  Some are supplied by the calling code in the BodyArguments array and some are generated automatically.  The automatically generated elements can be referred to by name.
HTML
Edit|Remove
html
<!DOCTYPE html> 
<html xmlns="http://www.w3.org/1999/xhtml"<head    <title>Thanks for Registering</title> 
    <link rel="stylesheet" type="text/css" href="{{StylesUrl}}" /> 
</head> 
<body    <div class="featured"        <hgroup class="title"            <h1>All most finished...</h1> 
        </hgroup> 
        <p            Thanks for registering with us {0}, Please complete your registration by clicking 
            <a href="{{VerificationUrl}}">here</a>.  
            To cancel your registration, click <a href="{{CancelUrl}}">here</a> 
        </p> 
    </div> 
</body> 
</html> 
 
To keep the SendMail activity very generic, I moved the formatting of the message and merging of the arguments into
the FormatMailBody activity.  As you can see, when I want to use a generated value in the email such as the stylesheet URL in line 5 I place the keyword inside of double braces.  If I want to refer to one of the Body arguments that my code created, I just use the typical positional references as in line 13.

Step 3: Run the Workflow

Rather than ask the MVC developer to become an expert on WorkflowApplication, I created a helper class which accepts the Workflow type that you want to use as a template parameter.  This allowed me to put in place a simple strongly typed API and hide the details of Workflow.  For the Workflow, I’ve created a StateMachine that does everything I need.  Of course, you can make the workflow more complex if you want.  I can imagine scenarios where a Human might have to approve membership or perhaps there is a membership fee that must be collected, any of these things can be provided for in the StateMachine.
And of course, I’ve added support for Debug Tracing of the Workflow as it executes using Microsoft.Activities.Extensions.  In the VS Debug window when the Workflow runs you will see nicely formatted trackiing information to help you.
41: Activity [1.34] "SendMail" scheduled child activity [1.90] "Wait For Confirmation"
42: Activity [1.34] "SendMail" scheduled child activity [1.62] "Sequence"
43: Activity [1.34] "SendMail" scheduled child activity [1.49] "Wait For Resend Command"
44: Activity [1.49] "Wait For Resend Command" is Executing
{
    Arguments
        Command: SendMail
}
45: Activity [1.62] "Sequence" is Executing
46: Activity [1.62] "Sequence" scheduled child activity [1.76] "Delay"
Problem – How to wait for a command to complete registration
I like to create an enum that declares the set of commands that I’m going to use for my Workflow.  In this case, there are a few simple commands.
C#
Edit|Remove
csharp
public enum RegistrationCommand 
{ 
    SendMail, 
    Confirm, 
    Cancel, 
} 
Then, I used the same technique that I demonstrated in the Introduction To StateMachine Hands On Lab.  I created an activity which waits for a command using the enum as the bookmark name.
Problem – How to monitor workflows with expired timers
For this example, I have decided against using Windows Server AppFabric because I want to (eventually) run this on Windows Azure.  However it is quite simple to plug in the monitoring by launching a thread from the Application_Start method.
C#
Edit|Remove
csharp
protected void Application_Start() 
{ 
    AreaRegistration.RegisterAllAreas(); 
 
    // Use LocalDB for Entity Framework by default 
    Database.DefaultConnectionFactory = 
        new SqlConnectionFactory( 
            "Data Source=(localdb)\v11.0; Integrated Security=True; MultipleActiveResultSets=True"); 
 
    RegisterGlobalFilters(GlobalFilters.Filters); 
    RegisterRoutes(RouteTable.Routes); 
 
    // TODO: Notice how we monitor registrations with durable timers 
    RegistrationVerification<AccountRegistration>.MonitorRegistrations(); 
 
    BundleTable.Bundles.RegisterTemplateBundles(); 
} 

 Setup

Pre-Requisites
The sample code requires
Configuration
Just look for TODO in the web.config file to find things.
You will first need to create the Workflow Instance Store database.  I’ve provided some batch files to make things easier
CreateInstanceStore.cmd – Drops / Creates the instance store.  Close IISExpress prior to running this to close the connection.
Reset.cmd – Removes all users from the ASP.NET Membership store, removes / recreates c:\mailbox and re-creates the instance store. 
The email is configured to drop messages into c:\mailbox, however you can modify the config to use hotmail or your favorite email provider if you like.
The <appSettings> group includes two values
ReminderDelay – The timespan that the workflow will wait before sending a reminder email.  For testing you should make this a small value.  However keep in mind that after three reminders the account will be deleted so if you are debugging you should make this value longer.
InstanceDetectionPeriod – The number of seconds that the InstanceStore will wait before polling the database for changes.

Try It

  1. Press F5 to debug the app
  2. Register a new user
  3. Check the C:\Mailbox folder for an email message
  4. Open the message and click on the confirm link
  5. The registration will complete
Try other variations like not confirming or trying to log in before you have confirmed etc.