Decoupling SimpleMembership From Your ASP.NET MVC Application

I recently came across a question in StackOverflow on making SimpleMembership "N-Tier Friendly".  The author of the question correctly pointed out that the code generated by the Internet template tightly couples SimpleMembership with the rest of the web application, peppering the application with code at the database level. What the author was really looking for was if anyone had taken the time to write SimpleMembership as a separate layer that he could reuse. My answer was that SimpleMembership was designed to be highly customizable and therefore any library written to decouple SimpleMembership from the application would not be reusable. That being said I thought it would be a good exercise to provide an example of how you could go about writing a library that abstracts SimpleMembership and has the features that we have covered so far in this blog, such as email confirmation and basic authentication.  And since I am making the source code available anyone can take this code as a starting point and customize it to their application requirements.

I have put the VS 2012 projects for each tutorial on SimpleMembership in GitHub where anyone can download them.

So what did I mean when I stated that the template generated code peppers the application code with database specific information.  One example is in the initialization code for InitializeSimpleMembershipAttribute.

private class SimpleMembershipInitializer
{
    public SimpleMembershipInitializer()
    {
        Database.SetInitializer<userscontext>(null);

        try
        {
            using (var context = new UsersContext())
            {
                if (!context.Database.Exists())
                {
                    // Create the SimpleMembership database without Entity Framework migration schema
                    ((IObjectContextAdapter)context).ObjectContext.CreateDatabase();
                }
            }

            WebSecurity.InitializeDatabaseConnection("DefaultConnection", "UserProfile", "UserId", "UserName", autoCreateTables: true);
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException("The ASP.NET Simple Membership database could not be initialized. For more information, please see http://go.microsoft.com/fwlink/?LinkId=256588", ex);
        }
    }
}

As you can see from this code DbContext and other EF classes are called directly to setup initialization of the database.  When I write MVC applications I like to use several design patterns to encapsulate the database layer from the application domain. One is the Repository Design Pattern, which encapsulates which ORM we are using. This allows for easily swapping out EF for another ORM, and for using mock-ups during unit testing.  And I usually use Unit of Work Design Pattern to handle transactions.  With this in mind I created an assembly called SimpleSecurity which encapsulates SimpleMembership using these design patterns. If you look at the SimpleSecurity source code you will see a Repositories folder/namespace that contains a repository for UserProfile and a unit of work class. We will add more repositories in later posts that will add more functionality for SimpleSecurity.

SimpleSecurity is an application layer that encapsulates the methods in WebSecurity as well as access to the UserProfile.  So what does our initialization process look like when using SimpleSecurity. We remove the use of  InitializeSimpleMembershipAttribute and put it in the Global.asax Application_Start method

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    WebApiConfig.Register(GlobalConfiguration.Configuration);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
    AuthConfig.RegisterAuth();
    WebSecurity.Register();
}

As you can see from this example you just call WebSecurity.Register. SimpleSecurity uses the same class name of WebSecurity as WebMatrix so you just have to change the uses clauses to SimpleSecurity and remove WebMatrix and most of the code will work the same as in your original app. So here are the steps for using SimpleSecurity.

  1. Include SimpleSecurity in your MVC project
  2. Remove all uses statements for WebMatrix and replace them with SimpleSecurity.
  3. Remove  InitializeSimpleMembershipAttribute.cs from your MVC project.
  4. Remove the reference to InitializeSimpleMembershipAttribute in the AccountConroller.
  5. Add WebSecurity.Register() to the Global.asax, as shown above.
  6. Remove UsersContext and UserProfile classes from AccountModels.cs.
  7. Remove all references to UsersContext in the AccountController and replace them with a SimpleSecurity.WebSecurity method.

On the last item you will find code like this in the AccountController


   // Insert a new user into the database
    using (UsersContext db = new UsersContext())
    {
        UserProfile user = db.UserProfiles.FirstOrDefault(u => u.UserName.ToLower() == model.UserName.ToLower());
        // Check if user already exists
        if (user == null)
        {
            // Insert name into the profile table
            db.UserProfiles.Add(new UserProfile { UserName = model.UserName });
            db.SaveChanges();

            OAuthWebSecurity.CreateOrUpdateAccount(provider, providerUserId, model.UserName);
            OAuthWebSecurity.Login(provider, providerUserId, createPersistentCookie: false);

            return RedirectToLocal(returnUrl);
        }
        else
        {
            ModelState.AddModelError("UserName", "User name already exists. Please enter a different user name.");
        }
     }


Which now becomes this.

        try
        {
            WebSecurity.CreateUser(new UserProfile() 
                { UserName = model.UserName });
            OAuthWebSecurity.CreateOrUpdateAccount(provider, providerUserId, model.UserName);
            OAuthWebSecurity.Login(provider, providerUserId, createPersistentCookie: false);
        }
        catch (Exception ex)
        {
            ModelState.AddModelError("UserName", "User name already exists. Please enter a different user name.");
        }



And when we need to get information on the current user it will look like this.

  var user = WebSecurity.GetCurrentUser();
  ViewBag.Email = user.Email;


Now we have a clean layer that decouples our security data from the rest of the MVC application. I will use this as a basis for future enhancements to SimpleMembership going forward.   For example, I discussed a bug when deleting users when using WebMatrix.WebSecurity. Now we can implement a new version of DeleteUser in SimpleSecurity.WebSecurity that fixes this issue.  Watch this blog for further enhancements to SimpleSecurity.

Comments

Popular posts from this blog

Using Claims in ASP.NET Identity

Seeding & Customizing ASP.NET MVC SimpleMembership

Customizing Claims for Authorization in ASP.NET Core 2.0