Using Entity Framework Code First and ASP.NET Membership Together

Some time ago I was involved as a software designer and developer in an MVC 3 project that used Entity Framework Code First 4.1 in a repository layer. During development we postponed dealing with security as requirements were pretty simple (simple logons with a single Administrators role plus the requirement that users should only be able to see their own data). When we were close to deployment, we ran the aspnet_regsql tool against the database that EF had created for us to create the SQL schema and data that the Application Services such as Membership and Roles require. We also added an additional userName parameter to a Get method in the repository to filter records for the currently logged in user. Finally, we added Authorize attributes to a number of controllers to make sure they are only accessible for users that are logged in. We then deployed the application and database, created a number of users using the MvcMembership section we had added to the site, and announced the presence of the site. All of this worked great, but we ran into issues when started work on the next version of the application.

Dealing with Database Initializers

To make it easy during development to keep the database in sync with the Code First model we were using, we used the DropCreateDatabaseIfModelChanges strategy. With this strategy, the database gets dropped and recreated whenever the underlying model changes (e.g. you change the properties of an entity, change mappings, add a new entity and so on). You can create a class that inherits this base class and then override the Seed method to add some default records that you want available when the database is recreated. Here's a simple example that comes with this article's download.

public class MyDropCreateDatabaseIfModelChanges : DropCreateDatabaseIfModelChanges<PeopleContext>
{
  protected override void Seed(PeopleContext context)
  {
    context.People.Add(new Person 
        { FirstName = "Imar", LastName = "Spaanjaars", DateOfBirth = new DateTime(1971, 8, 9) });
    context.People.Add(new Person 
        { FirstName = "John", LastName = "Doe", DateOfBirth = new DateTime(1950, 1, 1) });
  }
}

This code simply creates two new Person instances, and adds them to the People collection. EF then takes care of persisting these objects in the database.

To make the Entity Framework aware of this class you have a few options. One option is to set a key called DatabaseInitializerForType in the appSettings section of web.config as follows:

<add key="DatabaseInitializerForType Namespace.To.Your.ObjectContext, AssemblyNameForThisType" 
     value="Namespace.To.Your.DropCreateDatabaseIfModelChangesClass, AssemblyNameForThisType" />

As an alternative you can call Database.SetInitializer and pass in an instance of your DropCreateDatabaseIfModelChangesClass class. I have implemented this strategy in the sample application. I wrapped this call in a static method in a separate class as follows:

public class PeopleContextInitializer
{
  public static void Init()
  {
    Database.SetInitializer(new MyDropCreateDatabaseIfModelChanges()); 
  }
} 

I then call the Init method from the Application_Start method in Global.asax:

protected void Application_Start()
{
  PeopleContextInitializer.Init();

  ...
}

Whenever the application starts, the correct initializer is fed to the Entity Framework which then uses it whenever it needs to recreate the database. This in turns causes the Seed method to fire, inserting the default records that the application requires.

However, since we added security to the application and blocked all controllers except the Login controller for anonymous users, we found that we could no longer log in after the database was recreated. Once the database is recreated, the required schema for the application services such as Membership and Roles is gone. What we wanted was a correctly configured Membership and Roles database, along with a default user and role. This would enable us to log in and perform tasks only available for logged in users. The first step to solving this issue was to programmatically add a user and role to the database. This is easy enough with code like this in Application_Start in the Global.asax:

const string password = "Testing123";
const string userName = "Administrator";
const string roleName = "Administrators";

if (!Roles.RoleExists(roleName))
{
  Roles.CreateRole(roleName);
}
if (Membership.GetUser(userName) == null)
{
  Membership.CreateUser(userName, defaultPassword);
  Roles.AddUserToRole(userName, roleName);
}

While this code runs fine against a database that has been prepared for the application services, it will crash when the required objects are not present in the database. When you use the membership services at the frontend (for example, when using the Login or CreateUserWizard controls in a Web Forms application), the database is created for you. However, when using the API to programmatically add users and roles, you need to make sure your database already has the database schema required by the application services. Fortunately, again this is pretty easy to do. In the sample application you find code that looks like this:

using System;
using System.Data.SqlClient;
using System.Web.Configuration;
using System.Web.Management;

namespace EFCodeFirstAndMembership.EF
{
  public static class ApplicationServices
  {
    readonly static string defaultConnectionString = 
          WebConfigurationManager.AppSettings["DefaultConnectionString"];
    readonly static string connectionString = 
          WebConfigurationManager.ConnectionStrings[defaultConnectionString].ConnectionString;
    readonly static SqlConnectionStringBuilder myBuilder = 
          new SqlConnectionStringBuilder(connectionString);

    public static void InstallServices(SqlFeatures sqlFeatures)
    {
      SqlServices.Install(myBuilder.InitialCatalog, sqlFeatures, connectionString);
    }

    public static void UninstallServices(SqlFeatures sqlFeatures)
    {
      SqlServices.Uninstall(myBuilder.InitialCatalog, sqlFeatures, connectionString);
    }
  }
}

This code gets the database connection for the application by retrieving its name from an appSetting called DefaultConnectionString. It then uses the security API to create the requested application services.

I can now easily create the schema with a single line of code from the Seed method in the DropCreateDatabaseIfModelChanges class as follows:

ApplicationServices.InstallServices(SqlFeatures.Membership | SqlFeatures.RoleManager); 

Now whenever the application starts up. the Init method is called which tells EF which database initialization strategy to use. When EF detects the model has changed it recreates the database and then calls Seed. The Seed method in turn creates the database schema for the application services. So, by the time the Init method in Global.asax is done, the database is ready and the code that inserts users and roles can successfully run. Or so I thought...

It turns out that Entity Framework doesn't create the database right away when you set the initializer. Instead, it uses a lazy loading strategy to postpone creating the model until the first time it's used. This posed a big problem for us. Each page in the web site is protected, so we needed the Membership login functionality before EF is ever used. In addition, the code that creates the user and the role also needs a properly set up database.

Fortunately, this is also easy to fix. You can force EF to create the database by calling Database.Initialize on an instance of your object context. In the sample application you find the code that's responsible for this in the PeopleContextInitializer class which looks like this:

public class PeopleContextInitializer
{
  public static void Init()
  {
    Database.SetInitializer(new MyDropCreateDatabaseIfModelChanges());   
    using (var db = new PeopleContext())
    {
      db.Database.Initialize(false);
    }    
  }
}

When Initialize is called, the database is created immediately. The parameter of the Initialize method can be used to force EF to recreate the model even if it had been created before. I have set it to false, as I only need it to run once. If you want a fresh database on each application start, you can set it to true instead.

The call to Initialize was the final piece of the puzzle. Now whenever my model changes, EF recreates the database, some default records are inserted, the database schema for the application services is set up correctly and a user and a role is created so I can log in immediately to the site.

Note that (some of) this may no longer be necessary when EF Code First migrations matures and becomes usable in production scenarios.

Downloads

You can download the full sample code for the demo application discussed in this article. Note: this application is used only to demonstrate these concepts. It does not demonstrate best practices for ASP.NET MVC or Entity Framework.

Download the full code (C# only)


Where to Next?

Wonder where to go next? You can post a comment on this article.

Doc ID 563
Full URL https://imar.spaanjaars.com/563/using-entity-framework-code-first-and-aspnet-membership-together
Short cut https://imar.spaanjaars.com/563/
Written by Imar Spaanjaars
Date Posted 08/20/2011 10:52

Comments

Talk Back! Comment on Imar.Spaanjaars.Com

I am interested in what you have to say about this article. Feel free to post any comments, remarks or questions you may have about this article. The Talk Back feature is not meant for technical questions that are not directly related to this article. So, a post like "Hey, can you tell me how I can upload files to a MySQL database in PHP?" is likely to be removed. Also spam and unrealistic job offers will be deleted immediately.

When you post a comment, you have to provide your name and the comment. Your e-mail address is optional and you only need to provide it if you want me to contact you. It will not be displayed along with your comment. I got sick and tired of the comment spam I was receiving, so I have protected this page with a simple calculation exercise. This means that if you want to leave a comment, you'll need to complete the calculation before you hit the Post Comment button.

If you want to object to a comment made by another visitor, be sure to contact me and I'll look into it ASAP. Don't forget to mention the page link, or the Doc ID of the document.

(Plain text only; no HTML or code that looks like HTML or XML. In other words, don't use < and >. Also no links allowed.