You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Feb 20, 2021. It is now read-only.
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 processthis.VerifyRegistration(model);
// TODO: Notice how we redirect to the confirmation pagereturnthis.Json(new { success = true, redirect = this.Url.Action("Confirmation") });
}
this.ModelState.AddModelError("", ErrorCodeToString(createStatus));
}
// If we got this far, something failedreturnthis.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
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><htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Thanks for Registering</title><linkrel="stylesheet"type="text/css"href="{{StylesUrl}}"/></head><body>
<divclass="featured">
<hgroupclass="title">
<h1>All most finished...</h1></hgroup><p>
Thanks for registering with us {0}, Please complete your registration by clicking
<ahref="{{VerificationUrl}}">here</a>.
To cancel your registration, click <ahref="{{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.
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
protectedvoid 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();
}
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
Press F5 to debug the app
Register a new user
Check the C:\Mailbox folder for an email message
Open the message and click on the confirm link
The registration will complete
Try other variations like not confirming or trying to log in before you have confirmed etc.