Extending the Microsoft ASP.NET Membership Provider

Microsoft did a wonderful thing when they gave us the ASP.NET Membership Provider.  With a single command line call to aspnet_regsql.exe, we can have a fully implemented membership system providing persistence of usernames, one-way encrypted passwords, question / answer combinations, account lockout, roles, personalization, and all sorts of other useful (and not-quite-so-useful) features.

However, there is also a bit missing.  Most websites will also want to store a bit more information about their users, such as first / last names, maybe their address, birthdate, favorite color, etc, etc.  For Microsoft to have supplied fields for all of this would have made things quite unwieldy, to say the least, but fortunately there is an easy way to extend the provider.

DISCLAIMER: Before I begin, let me say that this is not the only way to extend the provider, nor is it necessarily the best.  It is quite simple, however, and therein lies it’s appeal…

Database Tables

Once you have decided which fields you wish to add to the membership provider, create a table that includes these fields along with an ID field (you can call it what you want, but ID or UserID are obvious choices) of type uniqueidentifier.

The value in the ID field will be used to link to the ASP.NET Membership Provider tables (the UserId field in aspnet_Users).  You can create a relationship between these fields if you want, but I don’t worry about it as it saves messing with the auto-generated tables.  The table structure I am using for this example is as shown below:

databasetables

MyUser class

Create a class for your membership provider, called something like MyUser.  It’s structure should be something like the following:

public class MyUser
{
    public Guid ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public string FullName
    {
        get { return FirstName + " " + LastName; }
    }

    public MembershipUser User
    {
        get { return Membership.GetUser(ID); }
    }

    public static bool IsLoggedIn
    {
        get
        {
            MembershipUser u = null;
            try
            {
                u = Membership.GetUser();
            }
            catch
            {
            }

            return (u != null);
        }
    }

    public static bool IsAdmin
    {
        // Assumes a role called "Admin"
        get { return (IsLoggedIn && Roles.IsUserInRole("Admin")); }
    }

    public bool IsUserAdmin
    {
        get { return (Roles.IsUserInRole(User.UserName, "Admin")); }
    }

    private static UsersService _service;

    private static UsersService Service
    {
        get
        {
            if (_service == null)
                _service = new UsersService();

            return _service;
        }
    }

    public static MyUser GetUser(Guid userId)
    {
        return Service.GetUser(userId);
    }

    public static MyUser GetCurrentUser()
    {
        try
        {
            return Service.GetUser((Guid)Membership.GetUser().ProviderUserKey);
        }
        catch
        {
            return null;
        }
    }

    public static MyUser AddNewUser(string username, string password, string email, string firstName, string lastName, out MembershipCreateStatus createStatus)
    {
        MembershipUser u = Membership.CreateUser(username, password, email, null, null, false, out createStatus);
        if (u != null)
        {
            MyUser user = Service.AddUser((Guid)u.ProviderUserKey, firstName, lastName);
            return user;
        }
        else
            return null;
    }

    public static MyUser ConfirmUser(Guid id)
    {
        var user = Service.GetUser(id);

        if (user != null)
        {
            if (user.User.IsApproved)
                user = null;
            else
            {
                var mu = user.User;
                mu.IsApproved = true;
                Membership.UpdateUser(mu);
            }
        }
        return user;
    }
}

Okay, there’s quite a bit there, so let’s have a look at the various parts.

Lines 3-5 provide the extended field set we want, including the Guid field (which maps to the SQL uniqueidentifier) to link to the ASP.NET Membership Provider.  Lines 7-10 just provide a quick way to get a user’s full name, while lines 12-15 provides a simple way to get to the ASP.NET Membership User.

Following this are a few static and instance methods for getting the status of a user:  Lines 17-32 is a static call to see if anyone is currently logged in;  Lines 34-38 can be used to see if the currently logged in user is a member of the Admin role (you can use several of these methods for different roles), while lines 40-43 is an instance method to see if a particular user is a member of the Admin role.

Line 45 introduces a reference to a UsersService class.  We’ll be using this service class to provide access to our database repository, and I’ll provide full details below.  This is a static field, and lines 47-56 essentially provide a single property accessor to this field (sort of a Singleton, even though it’s all static).  Following this are several methods for CRUD (Create Read Update Delete – okay, there’s no Delete here, but you get the idea..).  The only surprise here is the ConfirmUser method.  I like users to confirm their email address by clicking a link in an email sent to them, so create their user as Unapproved.  When they click on a link in an email (which contains their UserID as a parameter), the ConfirmUser method is called with that ID, which updates their status.

Now, about that UsersService…

The UsersService class

The UsersService relies on a repository class, so let’s get that first.  I’m using Linq to SQL, so create a new LINQ to SQL class, and let’s call it LinqUsers.dbml.  Drag the User table (from the top of this post) onto the designer and check your namespaces if required.  I have named my data context ‘DB’ to make things easy.  Now, here’s the UsersRepository code…

public class UsersRepository
{
    public IQueryable<MyUser> GetUsers()
    {
        DB db = new DB();
        return from u in db.Users
               orderby u.LastName
               select new MyUser
               {
                   ID = u.ID,
                   FirstName = u.FirstName,
                   LastName = u.LastName
               };
    }

    public MyUser AddUser(Guid id, string firstName, string lastName)
    {
        MyUser result = null;

        using (DB db = new DB())
        {
            User user = new User();
            user.ID = id;
            user.FirstName = firstName;
            user.LastName = lastName;

            db.Users.InsertOnSubmit(user);
            db.SubmitChanges();

            result = GetUsers().ByID(id);
        }

        return result;
    }

    public bool RemoveUser(Guid id)
    {
        bool result = false;

        using (DB db = new DB())
        {
            var del = from u in db.Users
                      where u.ID == id
                      select u;

            db.Users.DeleteAllOnSubmit(del);
            db.SubmitChanges();

            result = true;
        }

        return result;
    }
}

The only thing this needs is the ByID() filter, as follows:

public static class UserFilters
{
    public static MyUser ByID(this IEnumerable<MyUser> qry, Guid id)
    {
        return (from u in qry
                where p.ID == id
                select p).SingleOrDefault();
    }
}

Now, the UsersService class can be defined as follows:

public class UsersService
{
    UsersRepository _repository = new UsersRepository();

    public IList<MyUser> GetUsers()
    {
        return _repository.GetUsers().ToList();
    }

    public MyUser GetUser(Guid id)
    {
        return _repository.GetUsers().ByID(id);
    }

    public MyUser AddUser(Guid id, string firstName, string lastName)
    {
        return _repository.AddUser(id, firstName, lastName);
    }

    public bool RemoveUser(Guid id)
    {
        MyUser user = _repository.GetUsers().ByID(id);
        if (user == null)
            return false;

        MembershipUser mu = user.User;
        _repository.RemoveUser(id);
        return Membership.DeleteUser(mu.UserName, true);
    }
}

This should all be fairly self explanatory.  The reference to the UsersRepository could be taken through a constructor, and the UsersRepository should really implement an IUsersRepository interface, so that IOC and Dependency Injection can be used to enable unit testing, but if you need it, you’ll know how to do it (yes, I do it on my own code, so don’t beat me…).

So that’s it, really.  While there is a fair whack of code in there, it’s all fairly simple stuff.  Linq to SQL makes the database work easy, and the database and MyUser class are easily extendable to provide for the management of more fields and roles.  Feel free to leave comments if this has helped, or to offer suggestions for how this could be improved.

About these ads

3 responses to “Extending the Microsoft ASP.NET Membership Provider

  1. Is it possible to extend these clases to have a hierarchy? ie a user can have a role and that role can manager other roles?

  2. Thanks for the starter code. It was easy to follow but could be made far less complex. If you simply make your class a partial on the linq table object, you can cut out much of your code and complexity. In fact, everything almost takes care of itself after that, including db updates.

  3. Great post.

    I like so much the idea of using extension methods to add filtering to the repository class. very elegant.

    Thanks!

    By the way, I’m guessing how we can implement sorting methods upon MyUser class. sorting by the MyUser fields is straightforward, but doing it by any of the MembershipUser fields would require to to kick the membershipProvider to obtain the MembershipUser before sort. -a common ORM’s N+1 problem-. I think we would need to extend the membershipProvider for such end. but what do you think?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s