MongoDB ASP.NET Membership Provider and Repository

I’m continuing to plug along on my little project. One of the decision points I had was: should I handle authentication and authorization with a standard SQL server (since that is built-in, with the ASP.NET Provider Model) – and then use MongoDB for my application data?

If I did that, then infrastructure-wise, I’d need a real instance of SQL Server AND I’d need a Mongo instance, so I just decided to do everything in Mongo.

“That’s great!”, you say, “I bet there are already some implementations of a membership provider for Mongo on the Internet!”, you add.

And there is. This seems to be the popular one. Note though, that it has a 3-star rating. I don’t agree with how it’s written and virtually no part was reusable to me. So, I started down the rabbit hole to see how far I could get. Surprisingly, I actually got pretty far! Let me share what I’ve learned so far.

Testability:
First and foremost, I wanted to make sure my components were testable. Now, when you are working with Microsoft code written from the mid-2000’s, you will find it’s not very easy to test with unit tests. Luckily, I already wrote my MongoRepository in such a way where I can easily swap that out with a MockMongoRepository, which acts the same way, but it’s really just an in-memory list. More on that, in a minute.

So, this means that as I write my MongoMembershipProvider, I can do it while writing unit tests against it. I used that technique to get the basic functionality working. Then, I spent several hours working/fighting with Bootstrap to make a decent-looking registration page for my website. When I was done, I tried it and sure enough – I got a new record in the database, first time!!

Managing the Data Store Dependency:
Let me first take a step back and explain what I did to manage the connection to Mongo. I’m a fan of the Repository Pattern, in general. With Mongo, it’s even easier because ever record must have an ID field.

I already worked up a simple little Repository Pattern for MongoDB. It is a little different that a RBDMS-repository, but in a good way, because in Mongo every record has an _id field, as I was saying. That means that the repository interface can be as simple as this:

public interface IMongoRepository<TEntity>
    where TEntity : class, IMongoItem, new()
{
    TEntity GetById(ObjectId id);

    IEnumerable<TEntity> GetAll();

    void Insert(TEntity item);

    void Update(TEntity item);

    void Delete(ObjectId id);
}

 

And here is my working (but not necessarily complete) abstract implementation of a Mongo repository:
public abstract class MongoRepositoryBase<T> : IMongoRepository<T>, IDisposable
    where T : class, IMongoItem, new()
{
    protected MongoRepositoryBase(MongoUrl url, String databaseName, String collectionName)
    {
        if (url == null)
            throw new ArgumentNullException("url");
        if (String.IsNullOrEmpty(databaseName))
            throw new ArgumentException("Argument "databaseName" cannot be null or empty.", "databaseName");
        if (String.IsNullOrEmpty(collectionName))
            throw new ArgumentException("Argument "collectionName" cannot be null or empty.", "collectionName");

        this.Url = url;
        this.DatabaseName = databaseName;
        this.CollectionName = collectionName;
    }

    protected MongoServer GetConnection()
    {
        if (client == null)
        {
            client = new MongoClient(Url);
            connection = client.GetServer();

            connection.Connect();
        }

        return connection;
    }
    MongoClient client = null;
    MongoServer connection = null;
    public MongoUrl Url { get; protected set; }

    public String DatabaseName { get; protected set; }

    public String CollectionName { get; protected set; }

    public T GetById(ObjectId id)
    {
        if (id == null)
            throw new ArgumentNullException("id");

        MongoServer connection = GetConnection();

        MongoDatabase database = connection.GetDatabase(DatabaseName);
        MongoCollection<T> collection = database.GetCollection<T>(CollectionName);

        T item = collection.FindOne(
            (IMongoQuery)new QueryDocument().Add("_id", id));

        return item;
    }

    public IEnumerable<T> GetAll()
    {
        MongoServer connection = GetConnection();

        MongoDatabase database = connection.GetDatabase(DatabaseName);
        MongoCollection<T> collection = database.GetCollection<T>(CollectionName);

        IEnumerable<T> items = collection.FindAll();

        return items;
    }

    public void Insert(T item)
    {
        if (item == null)
            throw new ArgumentNullException("item");

        MongoServer connection = GetConnection();

        MongoDatabase database = connection.GetDatabase(DatabaseName);
        MongoCollection<T> collection = database.GetCollection<T>(CollectionName);

        collection.Save(item);

    }

    public void Update(T item)
    {
        if (item == null)
            throw new ArgumentNullException("item");

        MongoServer connection = GetConnection();

        MongoDatabase database = connection.GetDatabase(DatabaseName);
        MongoCollection<T> collection = database.GetCollection<T>(CollectionName);

        collection.Save(item);
        connection.Disconnect();
    }

    public void Delete(ObjectId id)
    {
        if (id == null)
            throw new ArgumentNullException("id");

        MongoServer connection = GetConnection();

        MongoDatabase database = connection.GetDatabase(DatabaseName);
        MongoCollection<T> collection = database.GetCollection<T>(CollectionName);

        collection.Remove((IMongoQuery)new QueryDocument().Add("_id", id));
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (connection != null)
            {
                connection.Disconnect();
                connection = null;
            }
            if (client != null)
            {
                client = null;
            }
        }
    }
}

Just to be complete, that IMongoItem is nothing more than a simple interface like this:

public interface IMongoItem
{
    ObjectId Id { get; set; }
}

 

Remember how every record has an ID field? Well, if I have all of my data structures implement this interface, then I can easily crank out repositories for all of the collections/tables in my database. For example:

public class MongoMembershipUserRepository : MongoRepositoryBase<MongoMembershipUser>
{
    public const String DefaultDatabaseName = "MyDatabaseName";
    public const String DefaultCollectionName = "MembershipAuthentication";
    public MongoMembershipUserRepository(MongoUrl url, String databaseName, String collectionName)
        : base(url, databaseName, collectionName)
    {
    }

    public MongoMembershipUserRepository(MongoUrl url)
        : this(url, DefaultDatabaseName, DefaultCollectionName)
    {
    }
}

 

As you can see, since all of the code is handled in the abstract base class, I just need to handle the connection information.

Back to the MembershipProvider:
OK, so we have a mockable repository, so how do we actually swap that out so that we can use a fake one during testing? Well, it starts in how the constructor of the MembershipProvider is setup:

public class MongoMembershipProvider : MembershipProvider
{
    public MongoMembershipProvider(IMongoRepository<MongoMembershipUser> repository)
        : base()
    {
        if (repository == null)
            throw new ArgumentNullException("repository");

        this.repository = repository;
    }

    public MongoMembershipProvider()
        : this(new MongoMembershipUserRepository(
            new MongoUrl(ConfigurationManager.AppSettings["MongoDBUrl"])))
    {
    }

 

As you can see, if the ASP.NET run-time calls the empty constructor (or if we did for some reason), it is going to use the REAL repository and go to the config file to get the Mongo URI. However, we can pass in our own repository too! By the way, that MongoMembershipUser is just my own data structure for holding user data – it’s basically email, password, security question, answer, etc.

So, in my unit test class, I created a fake, in-memory repository which is only used for unit testing, so that I don’t have to hit the real database:

class MockMongoMembershipUserRepository : IMongoRepository<MongoMembershipUser>
{
    List<MongoMembershipUser> users = new List<MongoMembershipUser>();

    public MongoMembershipUser GetById(ObjectId id)
    {
        if (id == null)
            throw new ArgumentNullException("id");

        return users.Where(user => user.Id == id).FirstOrDefault();
    }
    public IEnumerable<MongoMembershipUser> GetAll()
    {
        return users;
    }
    public void Insert(MongoMembershipUser item)
    {
        if (item == null)
            throw new ArgumentNullException("item");

        users.Add(item);
    }
    public void Update(MongoMembershipUser item)
    {
        if (item == null)
            throw new ArgumentNullException("item");

        MongoMembershipUser user = GetById(item.Id);

        users.Remove(user);
        users.Add(item);
    }
    public void Delete(ObjectId id)
    {
        if (id == null)
            throw new ArgumentNullException("id");

        MongoMembershipUser user = GetById(id);

        users.Remove(user);
    }
}

 

What does that buy me?

Unit Testing my MembershipProvider:
This means that I can now do something like this in a unit test:

[TestMethod]
public void CreateUserWithValidArguments_ShouldReturnValidMembershipUser()
{
    IMongoRepository<MongoMembershipUser> repository =
        new MockMongoMembershipUserRepository();

    MongoMembershipProvider provider =
        new MongoMembershipProvider(repository);

    String userName = "jdoe@example.com";
    String password = "thePasword123";
    String passwordQuestion = "Favorite dog";
    String passwordAnswer = "Fido";
    Boolean isApproved = true;
    Object providerUserKey = new object();
    String emailAddress = "jdoe@example.com";
    MembershipCreateStatus status = MembershipCreateStatus.UserRejected;

    MembershipUser user =
        provider.CreateUser(userName, password, emailAddress,
            passwordQuestion, passwordAnswer, isApproved, providerUserKey, out status);

    Assert.IsNotNull(user);
}

 

Pretty cool! I create an instance of my fake/mock repository, and pass it into the membership provider – and its’ none-the-wiser!!

From this, I got the Register and Login functionality working (which are the most difficult), so the rest should be pretty easy. Meanwhile, I started getting a little paranoid about Mongo performance. Not so much if the database would perform well, but I wanted to know if this wrapper API I am using is doing things efficiently – and wanted to make sure I understood how to set up indexes in the database too.

MongoDB Lookup Performance:
OK, so here’s what I did. I used a unit test method to really connect to the database, and I used a for..next loop to generate a bunch of fake accounts. I inserted ~75,000, then inserted my “real” account, then put another 75,000 accounts after it.

Before I did this, I would obviously get sub-second response. After I did this though, I was getting 4 to 5 SECOND response! Uh oh.

Since my main .Where(..) clause when logging in is based on email and password, I tried creating an index (which is a good idea anyhow). That’s done by doing something like this, within Mongo:

db.MembershipAuthentication.ensureIndex({ "EmailAddress" : 1 , "Password" : 1})

 

That didn’t help. So, after some digging, it came down to my actual implementation. I was using a lambda expression and constructing my .Where(..) clause. Then, I would call .Count() and see if the count was greater than 0. This is the single line that was taking all the time!

So instead, I thought – why not just do a .FirstOrDefault()? If there is no record found, then it will be null. So, I changed the implemented to that:

public override Boolean ValidateUser(String username, String password)
{
    String hashedPassword = utility.HashUserPassword(password);

    IEnumerable<MongoMembershipUser> matchingUsers =
        repository.GetAll().Where(record =>
            record.EmailAddress == username &&
            record.Password == hashedPassword);

    MongoMembershipUser user = matchingUsers.FirstOrDefault();

    if (user != null)
    {
        user.LastActivityDate = DateTime.Now;
        user.LastLoginDate = DateTime.Now;

        repository.Update(user);
        return true;
    }
    else
    {
        return false;
    }
}

 

So now, with ~150,000 records in the database, I again have sub-second response. So, lesson-learned, doing a .Count() is a very slow operation for some reason!

Next Steps:
I need to finish up this MembershipProvider, but as you can see, all the major puzzle pieces are pretty much figured out – the rest is just a bunch of known-work. After that, it’s onto the RoleProvider which should be pretty similar.

Overall, I’m still very pleased with how easy it is to work with Mongo – and I’m actually enjoying learning Boostrap too. I am really very impressed with how easy they make it. The complaint I have there is that I haven’t found a very good resource for finding exactly what I need. So, with every question I ran across, it was :15 minutes of search, looking on StackOverflow, and Bootply. I’m taking notes, so it’s just regular learning-curve stuff – but I wish I could find a better, consolidated source to find out “how do you do X”.

Posted in ASP.NET, ASP.NET MVC, DI and IOC, MongoDB, NoSQL, Security, SQL, Uncategorized, Unit Testing

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

Archives
Categories

Enter your email address to follow this blog and receive notifications of new posts by email.

Join 2 other followers

%d bloggers like this: