Architecting software for change is a key element of software design as over any product's lifetime most time will be expended in enhancing and maintaining the product. This process requires more effort in the initial design and implantation stage but pays off in the long term when the product is released. The provider pattern is a powerful tool in the quest to achieve this goal as it allows the abstraction of key components of the product. This article will show how to implement a provider pattern for the database access of an ASP.NET Membership provider of which I assume the reader has knowledge. In this way I can build multiple database (DB) providers that will implement the database functionality that my Membership provider needs, and not have to build individual membership providers for each database. This might seem like overkill and in reality it probably is but the intent of the article is to explore the provider pattern and the Membership service is just the example that I choose..
I have been doing a lot of work in the last year with Object Relational Models such as nHibernate and have really developed a preference to using an ORM for my database work, as I am so much more comfortable working with objects than native SQL. For this article I chose Microsoft Linq to SQL (Dlinq) as my first provider and strongly typed Datasets as the second. Datasets are not an ORM but do provide an abstraction layer for the underlying database. Of course it would be simple to add other database providers such as nHibernate but I chose these two because the installation requirements were either minimal or none. The overall design of my solution looks like this:
The purpose of the article is to develop a database provider framework not a full-blown Membership provider so as such my Membership provider (MyMembershipProvider) only implements the following methods:
- ValidateUser
This checks that user exists and password is correct
- CreateUser
Create a new row in the membership table for the user
- GetUser
Populate the user object from the database if it exist
I then placed the following in my web.config file so that ASP.NET will make use of MyMembershipProvider which is contained in the DBMembershipProvider Assembly.:
<membership defaultProvider="DBProviderMembershipProvider">
<providers>
<add name="DBProviderMembershipProvider"
type="MyMembershipProvider,DBMembershipProvider"
description="DB Provider membership provider" />
</providers>
</membership>
A reason that one writes a membership provider in the first place is to add some functionality to the base such as the type of user. As a result I extended MembershipUser by adding a Type property and password to produce the MyUser class. The addition of the Type to my custom class is obvious but password is a little less so as I could find no way to set it in the Base class.
With this solution in place I can write code in my application such as:
//get the logged on User
MembershipUser u = Membership.GetUser(User.Identity.Name);
//exlicitly access my provider
MyMembershipProvider MyVersion = (MyMembershipProvider)Membership.Provider;
//use a method defined on my provider
IList<MyUser> list = MyVersion.GetUsersNameByType("Member");
I use Forms Authentication to gather new user information through a CreateNewUser.aspx Web Form that looks like:
When the Create User button is pressed the following method CreateUser_Click is called in our code behind that contains:
// Put user code to initialize the page here
//this return a boolean value - true or false
MembershipCreateStatus status;
MyObject mystuff = new MyObject(MembershipTypeList.SelectedValue);
//overloaded method to create user
Membership.CreateUser(UserName.Text, Password.Text, Email.Text, Question.SelectedValue, Answer.Text, true, mystuff, out status);
In this code I use MyObject to pass the Type of user to the provider and to return an error message if the database provider fails for any reason. My Validate method will be called when the user tries to Login and the code looks like:
public override bool ValidateUser(string username, string password)
{
// Validate input parameters
if (String.IsNullOrEmpty(username) ||
String.IsNullOrEmpty(password))
return false;
// Make sure the data source has been loaded
MyUser user = DBProviderFactory.Current.GetObject(username);
if (user == null)
{
string error = DBProviderFactory.Current.ErrorMessage();
return false;
}
if (user.password != password)
return false;
return true;
}
All this is standard stuff for any membership provider; however notice the line of code:
MyUser user = DBProviderFactory.Current.GetObject(username);
This is the key to our database provider model as we are abstracting the GetObject method and allowing its implementation to be provided by DLinq, Tableadapters or etc. Current is a property of DBProviderFactory that returns a DLinq or Tableadapter object that implement IDBProvider, depending on the ActiveORM appSetting in web.config.
public static IDBProvider Current
{
get
{
if (_Current == null)
{
if (System.Configuration.ConfigurationManager.AppSettings["ActiveORM"] == null ||
System.Configuration.ConfigurationManager.AppSettings["ActiveORM"] == "nHibernate")
_Current = Activator.CreateInstance(typeof(DLinqProvider)) as IDBProvider;
else
_Current = Activator.CreateInstance(typeof(TableAdapterProvider)) as IDBProvider;
}
return _Current;
}
}
The most important method is the CreateInstance that will instantiate the configured provider that implements our IDBProvider interface. Therefore if ActiveORM is set to DLinq then we will use the DLinqProvider implementation of GetObject.
The IDBProvider is a simple interface that looks like:
- GetObject will return a MyUser object for the input UserId.
- SaveUser will persist the MyUser object to the database.
- ErrorMessage return the error message if the provider throws.
DLinq Provider
DLinq (or Linq to SQL) a component of the LINQ Project, provides a run-time infrastructure for managing relational data as objects without giving up the ability to query. It does this by translating language-integrated queries into SQL for execution by the database and then translating the tabular results back into objects you define. Your application is then free to manipulate the objects while DLinq stays in the background tracking your changes automatically. I hesitate to say that it is a full-blown ORM as I am not sure if that is Microsoft's intentions as they talk of using Linq on top of other ORM's like NHibernate. I had to install the Microsoft Visual Studio Code Name "Orcas" Language-Integrated Query, May 2006 Community Technology Preview. The DLinq implementation was straightforward except for one compilation issue that arose from having my DLinq code in a subproject (DBMembershipProvider) as I wanted for my design. This issue was resolved on the DLinq forum as it was pointed out that I needed to modify my csproj file as follows:
"<Import Project="$(MSBuildBinPath)\Microsoft.CSHARP.Targets" /> becomes "<Import Project="$(ProgramFiles)\LINQ Preview\Misc\Linq.targets" />.
Remember DLinq is a technology preview as such there are issues like this but the Microsoft team seems responsive. As you can see from my implementation I was able to fulfill the requirements of the interface with no SQL. DLinq really looks promising especially when you realize the potential of the Lambda functions and Linq to Entities but these are subjects for other articles. With Dlinq one provides the mapping between the SQL table and a class through either an XML mapping file or decorating the class, I chose the later method. I would have liked to have decorated the MyUser class directly but it extends MembershipUser and I could not make this solution work. So I wrote a custom class, UserToDB for the sole purpose of persisting data to and from the MemberInfo table and its member data is moved to and MyUser class. The decorated class looks like:
[Table(Name="MemberInfo")]
public class UserToDB
{
protected int _Id = 0;
string _Username;
string _Email;
string _Password;
string _PasswordQuestion;
string _Type;
public UserToDB()
{
}
[Column(Id=true,AutoGen=true)]
public int Id
{
get { return _Id; }
set { _Id = value; }
}
[Column]
public virtual string username
{ . . . //other methods
-
The Table decorator provides the link between the class and the SQL table .
-
The Id has extra Column attributes as it is my primary key and I had to add the Autogen option otherwise DLinq threw with an "Cannot insert explicit value for identity column in table 'MemberInfo' when IDENTITY_INSERT is set to OFF" exception.
The Column defines the relation between a property and its column in the table for a simple property.
The GetObject implementation for this provider is simple and I first define a typed DataContext (UsersDB) which is much like a ADO.NET connection (even contains one) and is the conduit to and from the database and your class.
public class UsersDB : DataContext
{
public Table<MyUser> Users;
public UsersDB(string connection) : base(connection) { }
}
I then use the UsersDB to implement the GetObject method of the interface (note that it produces a MyUser class from the MemberInfo columns read into the UserToDB object :
public MyUser GetObject(string userID)
{
UsersDB db = new UsersDB(_connectionString);
// Get a typed table to run queries
try
{
UserToDB usertodb = db.Users.Single(c => c.username == userID);
MyUser User = new MyUser("DBProviderMembershipProvider",
usertodb.username,
null,
usertodb.email,
usertodb.passwordQuestion,
"",
true,
false,
DateTime.Now,
DateTime.Now,
DateTime.Now,
DateTime.Now,
DateTime.Now,
usertodb.password,
usertodb.Type,
usertodb.Id);
return User;
}
catch (Exception e)
{
//Dlinq throws an error if the row does not exist !!!
if (e.Message == "Sequence contains no elements")
return null;
_ErrorMessage = e.Message;
return null;
}
return null;
}
The return db.Users.Single(c => c.username == userID) looks a little weird as it is using a 'Lambda' function, a new feature in C# 3.0 that comes out of the functional lanquage world. Lambda functions are heavily used by DLinq querys and in this case we are returning a single MyUser entity class that has username equal to the userID. For more information on Lambdas .
The SaveUser code transfers the new user to a new row in the database table :
public bool SaveUser(MyUser user)
{
UsersDB db = new UsersDB(_connectionString);
UserToDB usertodb = new UserToDB(user);
db.Users.Add(usertodb);
db.SubmitChanges();
return true;
}
Dataset Provider
As the alternate provider I have chosen ASP.NET Datasets as they too can be used to provide an abstraction layer on top of the database and are a good alternative to an ORM .My methods consume and create MyUser objects directly as the DLinq implementation. I added a dataset using the DataSet Designer to my project that looks like:
I manually added the GetDataByUserId so that I had the query needed for my provider. All of the definition steps are done through the designer and for a great tutorial on this I refer the reader to Scott Guthrie's blog.
The Dataset's implementation of the GetObject method looks like:
public MyUser GetObject(string userID)
{
MemberInfoTableAdapter MemberAdapter = new MemberInfoTableAdapter();
MemberDS.MemberInfoDataTable MemberTable = MemberAdapter.GetDataByUserId(userID);
//does the userid exist in database?
if (MemberTable.Count == 0)
return null;
MyUser user = new MyUser(MemberTable[0].username, MemberTable[0].email, MemberTable[0].password, MemberTable[0].Type);
return user;
}
As you can see it just uses the GetDataByUserId method of our TableAdapter then creates and returns a new user object with the results of the row obtained from the database. There should only be one of these
My SaveUser code looks like:
MemberInfoTableAdapter MemberAdapter = new MemberInfoTableAdapter();
MemberAdapter.Insert(user.username,user.password,user.phone,user.email,user.Type);
Summary
As I mentioned in starting this article if only intended to explore the provider pattern in the context of a membership provider. It does however show how a membership provider can be written whose database layer is easily replaced and this allows us to adopt the use of an ORM like nHibernate and later replace it if it proves inadequate. We have the achieved our goal of designing for change.