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.
public ApplicationDbContext()
: base("DefaultConnection", throwIfV1Schema: false)
{
}
The first time we make a reference to ApplicationDbContext
, we're going to trigger the static
constructor.
static ApplicationDbContext()
{
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.
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.
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.
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";
Dictionary<string, string> roles = new Dictionary<string, string>();
roles.Add("role1", "Role 1");
roles.Add("role2", "Role 2");
roles.Add("role3", "Role 3");
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);
}
}
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);
}
var rolesForUser = userManager.GetRoles(user.Id);
if (!rolesForUser.Contains(adminKey)) {
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.
static ApplicationDbContext()
{
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.
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.
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.
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.
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. :)