Seeding & Customizing ASP.NET MVC SimpleMembership

In ASP.NET MVC 4 Microsoft introduced a new membership provider that is referred to as SimpleMembership.  SimpleMembership makes it easier to customize user profiles and incorporates OAuth support.  I have seen a lot of questions on StackOverflow on how to customize SimpleMembership and seed it with data and in this post I will discuss some methods for achieving both .

SimpleMembership uses Entity Framework (EF) 5 and the code-first model. To initialize the database that contains the users and roles a filter is used called InitializeSimpleMembershipAttribute.  This attribute is decorated on the AccountController as shown here:

[Authorize]
[InitializeSimpleMembership]
public class AccountController : Controller
{
   ...
}
 
This is setup this way so that the initialization code is only called when accessing the actions on the AccountController.  If you look at the code for InitializeSimpleMembershipAttribute you will see a lot of code to setup initialization which is basically there so that your MVC application will behave correctly if Forms Authentication is not used.  If you will always be using Forms Authentication we can eliminate most of this code and simplify things. I have not been able to verify it but I think the way that this code initializes the database is the reason I have seen inconsistent behavior where modifying the UserProfile class is not reflected in the database schema.  You can read more about this issue here.

In order to follow along with these directions for seeding and customizing SimpleMembership create a new MVC 4 project in Visual Studio and select the Internet option.  The first thing we do is remove the InitializeSimpleMembershipAttribute from the AccountController that is shown above.

Next we need to create our own database initializer.  Add a new class to the project called  InitSecurityDb.  Now add the following code to this class.

using System.Web.Security;
using SeedSimple.Models;
using WebMatrix.WebData;

namespace SeedSimple
{
    public class InitSecurityDb : DropCreateDatabaseIfModelChanges<userscontext>
    {
        protected override void Seed(UsersContext context)
        {
            
            WebSecurity.InitializeDatabaseConnection("DefaultConnection",
               "UserProfile", "UserId", "UserName", autoCreateTables: true);
            var roles = (SimpleRoleProvider)Roles.Provider;
            var membership = (SimpleMembershipProvider)Membership.Provider;

            if (!roles.RoleExists("Admin"))
            {
                roles.CreateRole("Admin");
            }
            if (membership.GetUser("test", false) == null)
            {
                membership.CreateUserAndAccount("test", "test");
            }
            if (!roles.GetRolesForUser("test").Contains("Admin"))
            {
                roles.AddUsersToRoles(new[] { "test" }, new[] { "admin" });
            } 

        }
    }
}

The call to WebSecurity.InitializeDatabaseConnection method is required to setup the database connections and information required by SimpleMembership.   After initializing the database we can seed it with the information we need to run our application in the overriden Seed method.  Here we create and Admin role and a test user and then map the role to this user.  Note that I have used the DropCreateDatabaseIfModelChanges type of initializer so that the database will only be created if our entities change. We will cover more on changing the UserProfile entity later on. You could change this to DropCreateDatabaseAlways if you want the database to always be recreated on application start-up.

To kick off this initialization we need to add some code to the Application_Start method in the Global.asax. Here is the code we need to add:

 Database.SetInitializer<userscontext>(new InitSecurityDb());
 UsersContext context = new UsersContext();
 context.Database.Initialize(true);
 if (!WebSecurity.Initialized)
    WebSecurity.InitializeDatabaseConnection("DefaultConnection",
         "UserProfile", "UserId", "UserName", autoCreateTables: true);


Notice that there is a check to make sure if WebSecurity is initialized and if it is not we go ahead and do it. We need to check this because our Seed method will only be called if the database model has changed.

That is all you need to do to seed the SimpleMembership database at application start-up.  Lets test this out by adding an AuthorizeAttribute on the Contact action in the HomeController.  The changes to the HomeController will look like this:

 [Authorize(Roles="Admin")] 
 public ActionResult Contact()
 {
      ViewBag.Message = "Your contact page.";
      return View();
 }

Compile and start your application in the debugger.  Click on the Contact tab.  Since we have added the Authorize attribute on this action it should take us to the logon page.  Enter the test user name and password that was seeded in the database and hit Enter. This should log you on and take you to the Contact tab.

You have now successfully seeded the SimpleMembership database.  Now lets see how to customize the user profile. To do this go to the Model folder and open AccountModels.cs.  In there is a class called UserProfile.  Lets modify it so that we can also store the email address for a user. Our code will look something like this:

 [Table("UserProfile")]
 public class UserProfile
 {
     [Key]
     [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
     public int UserId { get; set; }
     public string UserName { get; set; }
     public string Email { get; set; }
 }

Now run the application again. When you run the application EF will determine that the model has changed and will drop the database and recreate it.  To view the changes select "Show All Files" in the Solution Explores and you will see a database in the App_Data folder with the name specified in the web.confg for DefaultConnection.  Open the database and look in the UserProfile table.  You should see the new column titled Email as shown in this figure.


Here is some sample code to show we can access the email properties from UserProfile.

var context = new UsersContext();
var username = User.Identity.Name;
var user = context.UserProfiles.SingleOrDefault(u => u.UserName == username);
var email = user.Email;

That is all there is to seeding and customizing SimpleMembership security in your ASP.NET MVC application. This is much easier than the old method of creating custom membership and role providers.

You can download the source code for this Visual Studio 2012 project here.

To learn how to use these customization features to add email confirmation during registration to an ASP.NET MVC 4 Internet Application, read this post.

Comments

  1. This works alright, but what about when you want to add an extended profile using foreign keys. E.g Have an entity called Employee that belongs to a UserProfile. can this be done, i have also thought about Inheritance but only lasts as far as a thought, not like i have access to WebSecurity class

    ReplyDelete
    Replies
    1. You can link the user profile to other entities/table with foreign keys. This question gets asked a lot, so I am working on a example to demonstrate how to do this. Watch for future blog posts that demonstrate how.

      Delete
  2. Thanks for the great insights. Do you know what the difference is between:

    var membership = (SimpleMembershipProvider)Membership.Provider;
    var roles = (SimpleRoleProvider)Roles.Provider;
    membership.CreateUserAndAccount("test", "test");
    roles.CreateRole("Admin");
    roles.AddUsersToRoles(new[] { "test" }, new[] { "admin" });

    And just using:

    WebSecurity.CreateUserAndAccount("Admin", "Password1");
    Roles.CreateRole("Admin");
    Roles.AddUserToRole("Admin", "Admin");

    These later classes are from:
    using WebMatrix.WebData;
    using System.Web.Security;

    Although they're not SimpleMembership proper, they seem to work fine, are easier, and have more overrides (e.g. AddUserToRole). I admit to not understanding the security provider hierarchy...

    ReplyDelete
  3. Hi, I've tried your solution I get this exception:

    System.NotSupportedException: Model compatibility cannot be checked because the database does not contain model metadata. Model compatibility can only be checked for databases created using Code First or Code First Migrations.

    when executing this:

    context.Database.Initialize(true);

    do you know why this happens?

    ReplyDelete
    Replies
    1. This error indicates that there is already a database with the same name that was not created with EF code-first. Manually delete this database and try it again.

      Delete
  4. Thanks, very helpful, saved me quite some time.

    ReplyDelete
  5. Really helpful article, thanks. Just wanted to clarify that if role names are case-sensitive (I believe they are), then one of your code snippets needs a minor fix:

    roles.AddUsersToRoles(new[] { "test" }, new[] { "admin" });

    The other references to the 'admin' role are spelled with an uppercase 'A'. Sorry if I've misunderstood the example.

    ReplyDelete
    Replies
    1. Actually roles are NOT case sensitive when using SimpleRoleProvider. I think this is just a carryover from the old ASP.NET role providers which worked the same way. You can test this out by logging in as "test" user and going to the Contact page which has an AuthorizeAttribute with the role set to "Admin". It will let the "test" user access this page, but not the user "joe" which has no roles assigned. Now change the role in the AuthorizeAttribute to "admin", and you will see the same behavior.

      Delete

Post a Comment

Popular posts from this blog

Using Claims in ASP.NET Identity

Customizing Claims for Authorization in ASP.NET Core 2.0