Code First EF 4.1 with the Altairis Membership/Role Provider

Problem with Current Membership/Role Provider

I’ve always thought the default membership/role provider for ASP.Net is a bit heavy in that it is targeted by default to a different database than the main application database and takes several additional steps to set up and deploy. I found the Altairis Web Security Toolkit on CodePlex and it has a nice, simple schema that is easy to integrate that into your application database. It is also available from Nuget as Altairis.Web.Security.

Working with the EF Magic Unicorn Edition

unicorn07The new Entity Framework Code First, also known as the “EF Magic Unicorn Edition” provides a nice way to generate the database and the ORM from POCO (Plain Old CLR Objects) classes that you write up front. This gives a nice “code first” approach where the domain objects drive the database rather than vice-versa (also known as “persistence ignorance”).

With the current edition of Magic Unicorn the database is constantly being regenerated during development, which interrupts the workflow to do the additional step of adding the membership/role tables. And even if that’s not too much trouble the membership/role tables aren’t accessible to the code first EF context for access by the application.

Code First Integration with the Drop-in Altairis Provider

One solution, besides writing a provider from scratch, is to create code first classes that generate the database schema that is expected by the drop-in membership provider.  That way the membership provider still works as expected without change, and the user/role classes are available to the application for doing administration and easily adding additional columns for the User table.

To implement this I did the following steps for an MVC3 project:

    1. Start a new MVC3 web application project.
    2. Use the NuGet console to download the following packages:
      Altairis.Web.Security
      EntityFramework
    3. Create User and Role classes and include them in a class that defines the EF context. In the context class I did a few special things to make sure that the many-to-many table for the roles was generated correctly:

public class ApplicationDB : DbContext
{
    public DbSet<User> Users { get; set; }
    public DbSet<Role> Roles { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder) {
        // Maps to the expected many-to-many join table name for roles to users.
        modelBuilder.Entity<User>()
        .HasMany(u => u.Roles)
        .WithMany(r => r.Users)
        .Map(m =>
        {
            m.ToTable("RoleMemberships");
            m.MapLeftKey("UserName");
            m.MapRightKey("RoleName");
        });
    }
}

Data may also be seeded into the initial database, for example to create some default roles. The commented out class definition can be utilized during development to recreate the database if the model changes:

// Change the base class as follows if you want to drop and create the database during development:
// public class DBInitializer : DropCreateDatabaseIfModelChanges<ApplicationDB> 
public class DBInitializer : CreateDatabaseIfNotExists<ApplicationDB> 
{
    protected override void Seed(ApplicationDB context) 
    {
        var roles = new List<Role>{
            new Role{RoleName = "Administrator"},
            new Role{RoleName = "User"},
            new Role{RoleName = "PowerUser"}
        };

        roles.ForEach(r => context.Roles.Add(r));
    }
}

The classes themselves are straightforward and can be decorated with attributes that integrate well with MVC:

public class User {
    [Key]
    [Required(ErrorMessage = "User Name is required")]
    [Display(Name="User Name")]
    [MaxLength(100)]
    public string UserName { get; set; }

    [Required]
    [MaxLength(64)]
    public byte[] PasswordHash { get; set; }

    [Required]
    [MaxLength(128)]
    public byte[] PasswordSalt { get; set; }

    [Required(ErrorMessage = "Email is required")]
    [MaxLength(200)]
    public string Email { get; set; }

    [MaxLength(200)]
    public string Comment { get; set; }

    [Display(Name = "Approved?")]
    public bool IsApproved { get; set; }

    [Display(Name = "Crate Date")]
    public DateTime DateCreated { get; set; }

    [Display(Name = "Last Login Date")]
    public DateTime? DateLastLogin { get; set; }

    [Display(Name = "Last Activity Date")]
    public DateTime? DateLastActivity { get; set; }

    [Display(Name = "Last Password Change Date")]
    public DateTime DateLastPasswordChange { get; set; }

    public virtual ICollection<Role> Roles { get; set; }

}

public class Role {
    [Key]
    [Display(Name = "Role Name")]
    [Required(ErrorMessage = "Role Name is required")]
    [MaxLength(100)]
    public string RoleName { get; set; }

    public virtual ICollection<User> Users { get; set; }
}

Additional properties could be added to the User class and they wouldn’t interfere with the operation of the membership provider as long a they are not “Required” properties.

I wrote an example ASP.Net MVC3 application and it is available for download at Github: Download Code

Update: I found an issue with database initialization if the model is changed and the EF data access is not initialized before the classic ADO.Net access provided by Altairis. See this post for more information.

14 thoughts on “Code First EF 4.1 with the Altairis Membership/Role Provider

  1. James Post author

    The membership provider in this example (Altairis) will automatically identify the User table as that’s how it has been defined. It’s possible that a different provider will look for a different table and columns so please note that this one is specific to Altairis. The Membership provider itself is defined in the web.config (see download code).

  2. James Post author

    @RWBrad: That’s a great question and it is definitely a chicken/egg thing as you’d like to create a user with an admin role to get started. I was working on a related issue on the follow-up post on how to force the DB re-creation before the membership provider is accessed as it can cause an error otherwise. It could be done by calling the membership provider directly in that method. I’ll try that and do a follow-up on it.

    EDIT: I did find that you could do this in the “Seed” class for the initializer. Here is an example:

    MembershipCreateStatus status = new MembershipCreateStatus();
    Membership.CreateUser(“admin”, “password”, “admin@user.com”);
    if (status == MembershipCreateStatus.Success) {
    // Add the role to it.
    User user = context.Users.Find(“admin”);
    Role role = context.Roles.Find(“Administrator”);
    user.Roles = new List();
    user.Roles.Add(role);
    }

  3. Adam Nagle

    I dug into the Altairis TableMembershipProvider Source Code and found another way to Seed user data. I needed to do it this way because my initializer lives down in my data access layer.

    using (var hmac = new System.Security.Cryptography.HMACSHA512())
    {
    passwordSalt = hmac.Key;
    passwordHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));

    var users = new List
    {
    new User{ UserName = “testuser”, Email = “testuser@gmail.com”,
    PasswordHash = passwordHash,
    PasswordSalt = passwordSalt,
    IsApproved = true,
    DateCreated = DateTime.Now,
    DateLastPasswordChange = DateTime.Now,
    Roles = context.Roles.Where(x=>x.RoleName == “User”).ToList()
    }
    };
    users.ForEach(x => context.Users.Add(x));

  4. James Post author

    @dc: Not on the User entity because it would cause the Membership provider to break on the create method. However, a workaround could be to add the properties as not required and then use a ViewModel for the controller/view to make the property required with an attribute in the ViewModel for the purpose of validation in the view. The ViewModel could then be mapped to the User Entity in the controller. There are tools such as Automapper that can help automate that mapping.

  5. James Post author

    @Michiel, Thanks for packaging that up, looks very cool! Great to have nice packages like that to start up a new project.

  6. Stuart

    Adding role(s) to user(s) when seeding the DB…. stuck on this one for a bit.
    Just what I was after:
    user.Roles = new List();

    Thanks.

  7. Tim

    finally i found your post.it does help me a lot ,thanks a lot ,but i have one more question?how to init the database with .mdf format instead of scf .i played around with this few hours,can’t figure out,please help me.

  8. James Post author

    Glad you got it figured out OK. You can control the database it creates or connects to in the connection string.

Comments are closed.