Click here to Skip to main content
15,881,898 members
Articles / Security / Identity
Tip/Trick

MVC and Identity Framework 2.0 (Part 3)

Rate me:
Please Sign up or sign in to vote.
4.29/5 (4 votes)
25 Jan 2015CPOL5 min read 18.2K   16   1
Creating our database context and extending roles

Introduction

This is the 3rd of a series of articles regarding MVC and the new Identity framework 2.0. If you are not very familiar with the basis, you can start reading the previous articles:

At the moment, we've been able to create the basic project and extend users. We changed a little bit the basic structure adding our own table names but the real customization starts now. First, we need to understand how the database context works in order to modify it. At the end, we will extend the last element, the role.

Database Initializer

Every time database access is required (in our case, the first time we will access the database will be registering an user or trying to login) we will access the class ApplicationDbContext which is an implementation of IdentityDbContext<IdentityUser>. We extended IdentityUser through ApplicationUser (default implementation). The context is created using a static method Create() handled by Owin (we won't talk about Owin in these articles anyway).

The static method instances the class and it calls the base constructor, we can see it always passes the name of the connection (we can change it of course) and a very important parameter throwIfV1Schema, it's just a flag that we're turning off but it throws an exception in case we migrated and we didn't set anything, it detects there's an old version of the Identity Framework schema and it doesn't continue.

C#
public ApplicationDbContext()
: base("DefaultConnection", throwIfV1Schema: false)
{
}

The first time we make a reference to ApplicationDbContext, we're going to trigger the static constructor.

C#
static ApplicationDbContext()
{
    // Set the database initializer which is run once during application start
    // This seeds the database with admin user credentials and admin role
    Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer());
}

This constructor initializes the database using the current database context using a strategy which is a IDatabaseInitializer<TContext> class. If we browse some built-in strategies, we will see the following cases:

  • CreateDatabaseIfNoExists
  • DropCreateDatabaseAlways
  • DropCreateDatabaseIfModelChanges
  • NullDatabaseInitializer
  • MigrateDatabaseToLatestVersion

And so forth. They're easy to understand, the default installation follows the destroy and create pattern if model changes, something is not very useful once we have more features in our project.

C#
public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext> 
{
    protected override void Seed(ApplicationDbContext context) {
        InitializeIdentityForEF(context);
        base.Seed(context);
    }
}

In my case, we had a very specific set of requirements that didn't let us migrate neither create databases on the deploy, we needed to create the tables first and just check in our initializer that the database is up and running, period. So I couldn't use any of these strategies and I had to write my own one. Here we go.

Creating a Database Initializer Strategy

If we want to define a different strategy, we have to write our own class that handles initialization. Luckily, it's a very simple process. We must inherit IDatabaseInitializer<TContext> on our class. The interface implementation is only one method (execute the strategy) InitializeDatabase.

We are using generics always so I don't have to mention constantly we need to use the generic type TContext for everything, we will receive the context once we implement it and the methods are called. In my case, I had to create a simple strategy that fails if the database doesn't exist.

C#
public class ValidateSecurityDatabase<TContext> : IDatabaseInitializer<TContext>
        where TContext : ApplicationDbContext 
{
        public void InitializeDatabase(TContext context)
        {
            if (!context.Database.Exists())
            {
                throw new ConfigurationException("Database does not exist. Cannot continue.");
            }
                InitializeIdentityForEF(context);
            }
        }
}

As you can see, I call a method called InitializeIdentityForEF. This is a very normal strategy to check roles missing, or an user we need always is present (like a super admin). It's not necessary but I decided to verify the roles there and check the admin user.

C#
public static void InitializeIdentityForEF(ApplicationDbContext db)
{
    var userManager = HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
    var roleManager = HttpContext.Current.GetOwinContext().Get<ApplicationRoleManager>();
    const string name = "admin@mydomaincom";
    const string password = "Admin@123456";

    /* VALIDATE IF ROLES PACKED WITH THIS APPLICATION EXIST  */
    Dictionary<string, string> roles = new Dictionary<string, string>();
    roles.Add("role1", "Role 1");
    roles.Add("role2", "Role 2");
    roles.Add("role3", "Role 3");

    // Create non-existing roles
    foreach (var _role in roles)
    {
        string roleName = _role.Key;
        var role = roleManager.FindByName(roleName);
        if (role == null)
        {
            role = new ApplicationRole(roleName);
            role.Description = _role.Value.ToString();
            var roleresult = roleManager.Create(role);
        }
    }
    /* END CREATE ROLES.  */

    var user = userManager.FindByName(name);
    if (user == null)
    {
        user = new ApplicationUser
        {
            UserName = name,
            Email = name,
            FirstName = "System",
            LastName = "Administrator"
        };
        var result = userManager.Create(user, password);
        result = userManager.SetLockoutEnabled(user.Id, false);
    }

   // Add user admin to Role Admin if not already added
    var rolesForUser = userManager.GetRoles(user.Id);
    if (!rolesForUser.Contains(adminKey)) // At least one administrator user must exist
    {
        var result = userManager.AddToRole(user.Id, adminKey);
    }
}

The code is not so different to the original one, but I added the roles check. Keep in mind I'm using an extended role here that will be explained in this article too.

After adding our strategy, we need to use it. Let's modify the ApplicationDbContext class and set our initializer instead of the default one.

C#
static ApplicationDbContext()
{
    // Set the database initializer which is run once during application start
    // This seeds the database with admin user credentials and admin role
    Database.SetInitializer<ApplicationDbContext>(new ValidateSecurityDatabase<ApplicationDbContext>());
}

Notes

Keep in mind my strategy is very particular and might be you don't have to use it. Find your best strategy. In my opinion, there are some dangerous results if you use any Create or DropAndCreate strategy. Let's imagine somebody accidentally modified the config file adding a letter or a number to the database name. You won't ever note it and if you have enough permissions, the website will create a new database with default permissions. DropAndCreate is not even something to consider. We have to be able to control the possible scenarios in a better way.

Extending Roles

Unlike the User that's extended in the default project, Roles are not. The default project uses IdentityUserRole which is the default class.

C#
public class IdentityRole : IdentityRole<string, IdentityUserRole>
{
    public IdentityRole()
    {
        base.Id = Guid.NewGuid().ToString();
    }
  
    public IdentityRole(string roleName) : this()
    {
        base.Name = roleName;
    }
}

Users and Roles are implemented using generics, it allows to extend it and the framework will handle them for us mapping new columns to the database.

Extending the role is also simple but we need to modify some classes in order to make it work due to the implementation uses IdentityUserRole. In fact, there's not class at all in our code, it's within the framework. So, let's implement our extension.

C#
public class ApplicationRole : IdentityRole
{
    public ApplicationRole() : base() { }
    public ApplicationRole(string name) : base(name) { }
    public string Description { get; set; }
} 

We inherit IdentityRole, implement both default constructors and extend it using a new field "Description". This is not so complex but it's a good example, you could extend it as much as possible.

The next step is to update references to IdentityRole. The role manager uses this class instead of our new one, we have to point to our new generated class.

C#
public class ApplicationRoleManager : RoleManager<ApplicationRole>
{
    public ApplicationRoleManager(
        IRoleStore<ApplicationRole,string> roleStore)
        : base(roleStore)
    {
    }
    public static ApplicationRoleManager Create(
        IdentityFactoryOptions<ApplicationRoleManager> options, IOwinContext context)
    {
        return new ApplicationRoleManager(
            new RoleStore<ApplicationRole>(context.Get<ApplicationDbContext>()));
    }
}

If you wrote some code to initialize the entity framework or you left the original code, you need to modify the method in the strategy that create or check roles. You can see it in the code we previously wrote.

C#
if (role == null)
{
    role = new ApplicationRole(roleName);
    role.Description = _role.Value.ToString();
    var roleresult = roleManager.Create(role);
}

After this modification, the rest is clearly the same we did before for users, you need to replace in every view and view model the reference to add the new property. I think it's not necessary to go further here as we already created some examples previously and it's trivial to modify roles.

Conclusion

We went through every stage of creating a new MVC project with identity framework 2.0 and set up a database strategy. We also extended users and roles and mapped our own tables. In the next article, we will go through a very interesting topic, integration with Facebook, Google and Twitter and capture of additional data. The base project makes it easy to set up a login, but that's all it does and it's not very useful in the real world to be honest. Saves a lot of code of course, but we always need more. :)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
Argentina Argentina
Software developer, amateur writer, sports fan. I like more travelling than breathing Smile | :) I play any existing sport on earth but I love soccer (forever). I'm working on bigdata now and the topic and future of this is more than exciting.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Humayun Kabir Mamun25-Jan-15 20:14
Humayun Kabir Mamun25-Jan-15 20:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.