November 2014

Volume 29 Number 11


Cutting Edge : Store User Data in ASP.NET Identity

Dino Esposito | November 2014

Dino EspositoASP.NET Identity in Visual Studio 2013 is a way to simplify the boring but essential tasks of managing user data and establishing a more effective membership system. Previously, I provided an overview of the ASP.NET Identity API (msdn.microsoft.com/magazine/dn605872) and examined its involvement with social networks and the OAuth protocol (msdn.microsoft.com/magazine/dn745860). In this article, I’ll expand on the extensibility points of ASP.NET Identity starting with the user data representation and underlying data store.

Lay the Groundwork

First, let’s create a new blank ASP.NET MVC project in Visual Studio 2013. All of the defaults the wizard provides are OK, but be sure to select the single user authentication model. The scaffolded code stores user data in a local SQL Server file with an auto-generated name with the following convention: aspnet-[ProjectName]-[RandomNumber]. The code also uses Entity Framework to access the user database in reading and writing. The user data representation is in the class ApplicationUser:

public class ApplicationUser : IdentityUser
{
}

As you can see, the ApplicationUser class inherits from the system-provided class IdentityUser. To customize the user representation, let’s start from here and add a new member to the class:

public class ApplicationUser : IdentityUser
{
  public String MagicCode { get; set; }
}

The name of the class—ApplicationUser in this example—isn’t mandatory and you can change it if you’d like. In this example, I’ve deliberately chosen the weird “magic-code” field to indicate the dual possibility. You can add fields that require a UI such as birth date or social security number or anything else you want to specify during registration. You can also add fields you need to have, but can calculate silently (for example, an app-specific “magic” code) when the user record is actually created. The ApplicationUser class includes by default the members listed in Figure 1.

Figure 1 Members Defined on the IdentityUser Base Class

Member Description
Id Unique auto-generated identifier (GUID) for the table. This field is the primary key.
UserName Display name of the user.
PasswordHash Hash resulting from the provided password.
SecurityStamp A GUID automatically created at specific points in the UserManager object lifetime. Typically, it’s created and updated when the password changes or a social login is added or removed. The security stamp generally takes a user information snapshot and automatically logs in users if nothing has changed.
Discriminator This column is specific to the Entity Framework persistence model and determines the class to which the particular row belongs. You’re going to have a unique discriminator value for each class in the hierarchy rooted in IdentityUser.

 

The fields listed in Figure 1, as well as any other fields you add programmatically in the ApplicationUser class definition, end up stored in a database table. The default table name is AspNetUsers. Also, the IdentityUser class exposes a few more properties such as Logins, Claims and Roles. These properties aren’t stored in the AspNetUsers table, but find their place in other side tables in the same database—AspNetUserRoles, AspNetUserLogins and AspNetUserClaims (see Figure 2).

Default Structure of the ASP.NET Identity User Database
Figure 2 Default Structure of the ASP.NET Identity User Database

Modifying the ApplicationUser class doesn’t ensure you’ll have the additional fields immediately reflected in the UI and saved to the database. Luckily, updating the database doesn’t take too much work.

Changes to the Scaffolded Code

You’ll need to edit the application views and models to reflect the new fields. In the application form where new users register with the site, you’ll need to add some markup to show the UI for the magic code and any other extra fields. Figure 3 shows amended code for the CSHTML Razor file behind the register form.

Figure 3 Razor File to Present Users with Extra Fields

@using (Html.BeginForm("Register", "Account",
  FormMethod.Post, new 
    { @class = "form-horizontal", role = "form" }))
{
  @Html.AntiForgeryToken()
  <h4>Create a new account.</h4>
  <hr />
  @Html.ValidationSummary()
  <div class="form-group">
    @Html.LabelFor(m => m.MagicCode, 
      new { @class = "col-md-2 control-label" })
    <div class="col-md-10">
      @Html.TextBoxFor(m => m.MagicCode, 
        new { @class = "form-control" })
    </div>
  </div>   
  ...
}

The CSHTML register form is based on a view model class, conventionally called RegisterViewModel. Figure 4 shows the changes required to the RegisterViewModel class to plug it into the classic validation mechanism of most ASP.NET MVC applications based on data annotations.

Figure 4 Changes to the Register View Model Class

public class RegisterViewModel
{
  [Required]
  [Display(Name = "User name")]
  public string UserName { get; set; }
  [Required]
  [StringLength(100, ErrorMessage = 
    "The {0} must be at least {1} character long.",
    MinimumLength = 6)]
  [DataType(DataType.Password)]
  [Display(Name = "Password")]
  public string Password { get; set; }
  [DataType(DataType.Password)]
  [Display(Name = "Confirm password")]
  [Compare("Password", ErrorMessage = 
    "Password and confirmation do not match.")]
  public string ConfirmPassword { get; set; }
  [Required]
  [Display(Name = "Internal magic code")]
  public string MagicCode { get; set; }
}

These changes aren’t enough, however. There’s one more step required and it’s the most critical. You have to pass the additional data down to the layer that will write it to the persistent store. You need to make further changes to the controller method that processes the POST action from the register form:

public async Task<ActionResult> Register(RegisterViewModel model)
{
  if (ModelState.IsValid) {
    var user = new ApplicationUser() { UserName = model.UserName,
      MagicCode = model.MagicCode };
    var result = await UserManager.CreateAsync(user, model.Password);
    if (result.Succeeded) {
      await SignInAsync(user, isPersistent: false);
      return RedirectToAction("Index", "Home");
    }
}

The key change is saving the posted data for the magic code (or whatever else you want to add to the user definition). This is saved into the ApplicationUser instance being passed to the CreateAsync method of the UserManager object.

Looking into the Persistent Store

In the sample code generated by the ASP.NET MVC template, the AccountController class has a member defined here:

public UserManager<ApplicationUser> UserManager { get; private set; }

The UserManager class is instantiated passing the user store object. ASP.NET Identity comes with a default user store:

var defaultUserStore = new UserStore<ApplicationUser>(new ApplicationDbContext())

Much like ApplicationUser, the ApplicationDbContext class inherits from a system-defined class (named IdentityDbContext) and wraps Entity Framework to do the actual persistence job. The great news is you can unplug the default storage mechanism altogether and roll your own. You can base your custom storage engine on SQL Server and Entity Framework and just use a different schema. You could also take advantage of a completely different storage engine such as MySQL or a NoSQL solution. Let’s see how to arrange a user store based on an embedded version of RavenDB (ravendb.net). The prototype of the class you’ll need is shown here:

public class RavenDbUserStore<TUser> :
  IUserStore<TUser>, IUserPasswordStore<TUser>
  where TUser : TypicalUser
{
  ...
}

If you intend to support logins, roles and claims, you’ll need to implement more interfaces. For a minimal working solution, IUserStore and IUserPasswordStore are enough. The class Typical­User is a custom class I created to remain decoupled from the ASP.NET Identity infrastructure as much as possible:

public class TypicalUser : IUser
{
  // IUser interface
  public String Id { get; set; }
  public String UserName { get; set; }
  // Other members
  public String Password { get; set; }
  public String MagicCode { get; set; }
}

At the very least, the user class must implement the IUser interface. The interface counts two members—Id and UserName. You’ll likely want to add a Password member, as well. This is the user class being saved in the RavenDB archive.

Add RavenDB support to your project via the RavenDB.Embedded NuGet package. In the global.asax, you’ll also need to initialize the database with the following code:

private static IDocumentStore _instance;
public static IDocumentStore Initialize()
{
  _instance = new EmbeddableDocumentStore 
    { ConnectionStringName = "RavenDB" };
  _instance.Initialize();
  return _instance;
}

The connection string points to the path where you should create the database. In an ASP.NET Web application, the natural fit is a subfolder under App_Data:

<add name="RavenDB" connectionString="DataDir = ~\App_Data\Ravendb" />

The user store class contains code for the methods in the IUserStore and IUserPasswordStore interfaces. These let the application manage users and related passwords. Figure 5 shows the store implementation.

Figure 5 A Minimally Working User Store Based on RavenDB

public class RavenDbUserStore<TUser> :
  IUserStore<TUser>, IUserPasswordStore<TUser>
  where TUser : TypicalUser
{
  private IDocumentSession DocumentSession { get; set; }
  public RavenDbUserStore()
  {
    DocumentSession = RavenDbConfig.Instance.OpenAsyncSession();
  }
  public Task CreateAsync(TUser user)
  {
    if (user == null)
      throw new ArgumentNullException();
    DocumentSession.Store(user);
    return Task.FromResult<Object>(null);
  }
  public Task<TUser> FindByIdAsync(String id)
  {
    if (String.IsNullOrEmpty(id))
      throw new ArgumentException();
    var user = DocumentSession.Load<TUser>(id);
    return Task.FromResult<TUser>(user);
  }
  public Task<TUser> FindByNameAsync(String userName)
  {
    if (string.IsNullOrEmpty(userName))
      throw new ArgumentException("Missing user name");
    var user = DocumentSession.Query<TUser>()
      .FirstOrDefault(u => u.UserName == userName);
    return Task.FromResult<TUser>(user);
  }
  public Task UpdateAsync(TUser user)
  {
    if (user != null)
      DocumentSession.Store(user);
    return Task.FromResult<Object>(null);
  }
  public Task DeleteAsync(TUser user)
  {
    if (user != null)
      DocumentSession.Delete(user);
    return Task.FromResult<Object>(null);
  }
  public void Dispose()
  {
    if (DocumentSession == null)
      return;
    DocumentSession.SaveChanges();
    DocumentSession.Dispose();
  }
  public Task SetPasswordHashAsync(TUser user, String passwordHash)
  {
    user.Password = passwordHash;
    return Task.FromResult<Object>(null);
  }
  public Task<String> GetPasswordHashAsync(TUser user)
  {
    var passwordHash = user.Password;
    return Task.FromResult<string>(passwordHash);
  }
  public Task<Boolean> HasPasswordAsync(TUser user)
  {
    var hasPassword = String.IsNullOrEmpty(user.Password);
    return Task.FromResult<Boolean>(hasPassword);
  }
 }
}

Any interaction with RavenDB passes through the opening and closing of a document store session. In the constructor of the RavenDbUserStore, you open the session and dismiss it in the Dispose method of the store object. Before dismissing the session, though, call the SaveChanges method to persist all pending changes in accordance with the Unit-of-Work pattern:

public void Dispose()
{
  if (DocumentSession == null)
    return;
  DocumentSession.SaveChanges();
  DocumentSession.Dispose();
}

The API to work with the RavenDB database is fairly simple. Here’s the code you’ll need to create a new user:

public Task CreateAsync(TUser user)
{
  if (user == null)
    throw new ArgumentNullException();
  DocumentSession.Store(user);
  return Task.FromResult<Object>(null);
}

To retrieve a given user, use the Query method on the Document­Session object:

var user = DocumentSession.Load<TUser>(id);

RavenDB assumes any class you persist has an Id property. If not, it will implicitly create such a property so you can always use the Load method to retrieve any object by Id, whether it’s your own ID or a system-generated ID. To retrieve a user by name, perform a classic query using a LINQ syntax:

var user = DocumentSession
              .Query<TUser>()
              .FirstOrDefault(u => u.UserName == userName);

Use ToList to select a variety of objects and store them into a manageable list. Dealing with passwords is equally simple. RavenDB stores passwords in a hashed format, but hashing is managed outside the RavenDB module. The SetPasswordHashAsync method, in fact, already receives the password hash the user provides.

Figure 5 is the full source code to set up a RavenDB user store compatible with ASP.NET Identity. It’s sufficient to log users in and out when you have the embedded version of RavenDB installed. For more sophisticated features such as external logins and account management, you need to implement all ASP.NET Identity user-related interfaces or signifi­cantly rework the code in the Account­Controller you get from scaffolding.

The Bottom Line

ASP.NET Identity lets you completely unplug the Entity Framework-based storage infrastructure and use RavenDB instead, a document and schema-free database. You can install RavenDB as a Windows service, an IIS application or embedded as I did here. Just write a class that implements a few ASP.NET Identity interfaces and inject this new store class into the UserManager infrastructure. Changing the user type schema is easy, even in the default configuration based on EF. If you use RavenDB, though, you get rid of any migration issues should the user format change.


Dino Esposito is the co-author of “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2014) and “Programming ASP.NET MVC 5” (Microsoft Press, 2014). A technical evangelist for the Microsoft .NET Framework and Android platforms at JetBrains and frequent speaker at industry events worldwide, Esposito shares his vision of software at software2cents.wordpress.com and on Twitter at twitter.com/despos.

Thanks to the following technical expert for reviewing this article: Mauro Servienti