Building Your Own Provider
The built-in SQL database functionality is adequate for most purposes. You may, however, want to build a provider for some other data store (such as a FoxPro database), or you might have your own existing database that you want to plug into this existing framework. Fortunately, the provider model enables you to pipe all of this Membership goodness into your own system by writing you own provider.Listing 11.3. Let's pretend that our data is stored in our own SQL Server database for the purpose of these code samples.
Figure 11.1. Tying Membership to the data store, with your provider in the middle.

Figure 11.2. Filling in the blanks with Intellisense.
[View full size image]

Listing 11.4. Properties in our derived MembershipProvider
C#
VB.NET
public class CustomMembershipProvider : MembershipProvider
{
private string _applicationName;
public override string ApplicationName
{
get { return _applicationName; }
set { value = _applicationName; }
}
private bool _enablePasswordReset;
public override bool EnablePasswordReset
{
get { return _enablePasswordReset; }
}
private bool _enablePasswordRetrieval;
public override bool EnablePasswordRetrieval
{
get { return _enablePasswordRetrieval; }
}
private string _name;
public override string Name
{
get
{
if (_name == null) return "MyMembershipProvider";
return _name;
}
}
private bool _requiresQuestionAndAnswer;
public override bool RequiresQuestionAndAnswer
{
get { return _requiresQuestionAndAnswer; }
}
}
Now that we have a handle on the properties, let's flesh out the required methods of our provider. The first one we'll need to write is called Initialize(), and it's actually an abstract member in System.Configuration.Provider.ProviderBase, the class that MembershipProvider in turn inherits from. This method is used to populate our read-only properties that we established previously by reading the matching values from web.config. Because the values from web.config are strings, we'll also add a private helper method to interpret Boolean values. The result is shown in Listing 11.5.
Public Class CustomMembershipProvider
Inherits MembershipProvider
Private _applicationName As String
Public Overrides Property ApplicationName() As String
Get
Return _applicationName
End Get
Set
value = _applicationName
End Set
End Property
Private _enablePasswordReset As Boolean
Public Overrides ReadOnly Property EnablePasswordReset() As Boolean
Get
Return _enablePasswordReset
End Get
End Property
Private _enablePasswordRetrieval As Boolean
Public Overrides ReadOnly Property EnablePasswordRetrieval() As _
Boolean
Get
Return _enablePasswordRetrieval
End Get
End Property
Private _name As String
Public Overrides ReadOnly Property Name() As String
Get
If _name Is Nothing Then
Return "MyMembershipProvider"
End If
Return _name
End Get
End Property
Private _requiresQuestionAndAnswer As Boolean
Public Overrides ReadOnly Property RequiresQuestionAndAnswer() As _
Boolean
Get
Return _requiresQuestionAndAnswer
End Get
End Property
End Class
Listing 11.5. The Initialize() method of our derived MembershipProvider
C#
VB.NET
public override void Initialize(string name, NameValueCollection config)
{
_name = name;
if (config["applicationName"] == null || config["applicationName"].Trim() == ")
{
_applicationName = HttpContext.Current.Request.ApplicationPath;
}
else
{
_applicationName = config["applicationName"];
}
_enablePasswordReset = GetBooleanValue(config["enablePasswordReset"], true);
_enablePasswordRetrieval = GetBooleanValue(config["enablePasswordRetrieval"], true);
_requiresQuestionAndAnswer = GetBooleanValue(config["requiresQuestionAndAnswer"], false);
}
private bool GetBooleanValue(string configValue, bool defaultValue)
{
if (configValue != null)
{
if (configValue.ToUpper() == "TRUE") { return true; }
if (configValue.ToUpper() == "FALSE") { return false; }
}
return defaultValue;
}
Public Overrides Sub Initialize(name As String, _
config As NameValueCollection)
_name = name
If config("applicationName") Is Nothing Or _
config("applicationName").Trim() = " Then
_applicationName = HttpContext.Current.Request.ApplicationPath
Else
_applicationName = config("applicationName")
End If
_enablePasswordReset = _
GetBooleanValue(config("enablePasswordReset"), True)
_enablePasswordRetrieval = _
GetBooleanValue(config("enablePasswordRetrieval"), True)
_requiresQuestionAndAnswer = _
GetBooleanValue(config("requiresQuestionAndAnswer"), False)
End Sub
Private Function GetBooleanValue(configValue As String, _
defaultValue As Boolean) As Boolean
If Not (configValue Is Nothing) Then
If configValue.ToUpper() = "TRUE" Then
Return True
End If
If configValue.ToUpper() = "FALSE" Then
Return False
End If
End If
Return defaultValue
End Function
The NameValueCollection is found in the System. Collections.Specialized namespace, and as such, you may need the proper using/Imports statements if you don't call it by its fully qualified name.Listing 11.3, you'll notice a number of attributes in the add element where we add a provider to the application. The attributes requiresUniqueEmail and passwordFormat are specific to the SqlMembershipProvider, so we don't have to implement those here (unless we want to). We'll still need to get the other five values and assign them to our properties though, and that's what the Initialize() method does. The name string parameter corresponds to the name we give the provider in web.config, while the NameValueCollection parameter config gets the rest of the values. Because the values are strings and some of our properties are Boolean, we use the private helper method GetBooleanValue() to convert the string to an all-uppercase value and interpret it as true or false, or return a default value we supply.Other methods need to speak the language of the Membership class. For example, GetUser() must return a MembershipUser object, so you'll need to write some plumbing code to convert your data to this expected format. In fact, you'll need to do the same thing for several other methods (such as GetAllUsers()), so a helper method that converts the contents of a SqlDataReader to a MembershipUser object is just what you'll need. That method and GetUser() are demonstrated in Listing 11.6.
Listing 11.6. The GetUser() method of our derived MembershipProvider, with the helper method PopulateMembershipUser()
C#[View full width]
VB.NET
public override MembershipUser GetUser(string name, bool userIsOnline)
{
MembershipUser user;
SqlConnection connection = new SqlConnection(ConfigurationSettings.ConnectionStrings["SqlServices"]. ConnectionString);
SqlCommand command = new SqlCommand("SELECT * FROM MyUsers WHERE Name = @Name", connection);
command.Parameters.AddWithValue("@Name", name);
SqlDataReader reader = command.ExecuteReader();
if (reader.Read()) user = PopulateMembershipUser(reader);
else throw new Exception("User not found");
reader.Close();
if (userIsOnline)
{
command.CommandText = "UPDATE MyUsers SET LastActivity = @LastActivity WHERE Name =@Name";
command.Parameters.AddWithValue("@LastActivity", DateTime.Now);
command.ExecuteNonQuery();
}
connection.Close();
return user;
}
private MembershipUser PopulateMembershipUser(SqlDataReader reader)
{
return new MembershipUser(this, reader["Name"].ToString(),
Convert.ToInt32(reader["ID"]), reader["Email"].ToString(),
reader["PasswordQ"].ToString(), reader["Comment"].ToString(),
Convert.ToBoolean(reader["Approved"]),
Convert.ToDateTime(reader["SignedUp"]),
Convert.ToDateTime(reader["LastLogin"]),
Convert.ToDateTime(reader["LastActivity"]),
Convert.ToDateTime(reader["LastPasswordChange"]));
}
The method starts by naming a MembershipUser variable that we'll return at the end of the method. This implementation of the GetUsers() method assumes that we have a table in our database called "MyUsers" and that it has columns named in the helper method called PopulateMembershipUser(). The first step is to create our typical connection and command objects, pass in a parameter to the command object with a name to match (specified in the method's name parameter), and finally execute a SqlDataReader. If a record is returned, checked by the reader's Read() method, we pass the reader to the helper method, which takes the values of the reader and returns them in a new MembershipUser object. One of the constructors of the MembershipUser class takes all of those values as parameters to populate the object, and that's what our helper method uses.Chapter 4, "Application Architecture."Let's try another one. UpdateUser() takes a MembershipUser object as a parameter, so in many ways it's the opposite of the GetUser() method. We'll take the properties of the MembershipUser that aren't read-only and persist those to the database. Listing 11.7 shows this code.
Public Overrides Function GetUser(name As String, _
userIsOnline As Boolean) As MembershipUser
Dim user As MembershipUser
Dim connection As New _
SqlConnection(ConfigurationSettings.ConnectionStrings("SqlServices"). ConnectionString)
Dim command As New SqlCommand(_
"SELECT * FROM MyUsers WHERE Name = @Name", connection)
command.Parameters.AddWithValue("@Name", name)
Dim reader As SqlDataReader = command.ExecuteReader()
If reader.Read() Then
user = PopulateMembershipUser(reader)
Else
Throw New Exception("User not found")
End If
reader.Close()
If userIsOnline Then
command.CommandText = _
"UPDATE MyUsers SET LastActivity = @LastActivity WHERE Name = @Name"
command.Parameters.AddWithValue("@LastActivity", DateTime.Now)
command.ExecuteNonQuery()
End If
connection.Close()
Return user
End Function
Private Function PopulateMembershipUser(reader As SqlDataReader) _
As MembershipUser
Return New MembershipUser(Me, reader("Name").ToString(),_
Convert.ToInt32(reader("ID")), reader("Email").ToString(), _
reader("PasswordQ").ToString(), _
reader("Comment").ToString(), _
Convert.ToBoolean(reader("Approved")), _
Convert.ToDateTime(reader("SignedUp")), _
Convert.ToDateTime(reader("LastLogin")), _
Convert.ToDateTime(reader("LastActivity")), _
Convert.ToDateTime(reader("LastPasswordChange")))
End Function
Listing 11.7. The UpdateUser() method of our derived MembershipProvider
C#[View full width]
VB.NET[View full width]
public override void UpdateUser(MembershipUser user)
{
SqlConnection connection = new SqlConnection(ConfigurationSettings.ConnectionStrings["myConnection"]. ConnectionString);
string sql = "UPDATE MyUsers SET Comment = @Comment, "
+ "SignedUp = @SignedUp, Email = @Email, Approved = @Approved, "
+ "LastLogin = @LastLogin, LastActivity = @LastActivity, "
+ "LastPasswordChange = @LastPasswordChange WHERE Name = @Name";
SqlCommand command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Comment", user.Comment);
command.Parameters.AddWithValue("@SignedUp", user.CreationDate);
command.Parameters.AddWithValue("@Email", user.Email);
command.Parameters.AddWithValue("@Approved", user.IsApproved);
command.Parameters.AddWithValue("@LastLogin", user.LastLoginDate);
command.Parameters.AddWithValue("@LastActivity", user.LastActivityDate);
command.Parameters.AddWithValue("@LastPasswordChange", user.LastPasswordChangedDate);
command.Parameters.AddWithValue("@Name", name);
command.ExecuteNonQuery();
connection.Close();
}
You have probably written data access code like this many times before. The only thing that's perhaps unique here is that the parameters you're passing in are properties of the MembershipUser parameter of the method. Calling Membership.UpdateUser(someMembershipUser) will call the method in your provider. Again, changing the data store means changing the provider, while any code that calls the static Membership methods need not be modified.The end result of all this is that the same code can get and modify user data, regardless of whether you use the Access, SQL Server, or a custom provider. The code in Listing 11.8 works exactly the same for every provider, as long as the code in the provider does what the method description indicates it should do.One feature we did not use here in our custom Membership provider was the ApplicationName property as it relates to our own data store. If we were to use the same database for multiple applications, we could use the value from ApplicationName as the connection between our data and the specific application. The default database could be used for any number of different applications because user records are marked with the application name.
Public Overrides Sub UpdateUser(user As MembershipUser)
Dim connection As New SqlConnection(ConfigurationSettings.ConnectionStrings("myConnection"). ConnectionString)
Dim sql As String = "UPDATE MyUsers SET Comment = @Comment, "
+ "SignedUp = @SignedUp, Email = @Email, Approved = @Approved, "
+ "LastLogin = @LastLogin, LastActivity = @LastActivity, "
+ "LastPasswordChange = @LastPasswordChange WHERE Name = @Name"
Dim command As New SqlCommand(sql, connection)
command.Parameters.AddWithValue("@Comment", user.Comment)
command.Parameters.AddWithValue("@SignedUp", user.CreationDate)
command.Parameters.AddWithValue("@Email", user.Email)
command.Parameters.AddWithValue("@Approved", user.IsApproved)
command.Parameters.AddWithValue("@LastLogin", user.LastLoginDate)
command.Parameters.AddWithValue("@LastActivity", user.LastActivityDate)
command.Parameters.AddWithValue("@LastPasswordChange", user.LastPasswordChangedDate)
command.Parameters.AddWithValue("@Name", name)
command.ExecuteNonQuery()
connection.Close()
End Sub
Listing 11.8. Using provider-neutral code with the Membership class
C#
VB.NET
MembershipUser user = Membership.GetUser("Jeff");
user.Email = "new@address.com";
Membership.UpdateUser(user);
We won't go through every method in MembershipProvider, but between the method names and descriptions in the documentation, it should be fairly obvious what each method should do. Remember that you'll also need to implement a class that inherits from RoleProvider to pipe the Roles class methods to your data store.After you have your custom providers for Membership and Roles ready to go, add the correct elements to web.config, as in Listing 11.9.
Dim user As MembershipUser = Membership.GetUser("Jeff")
user.Email = "new@address.com"
Membership.UpdateUser(user)
Listing 11.9. Setting web.config to use your custom providers
There are some differences here when using a custom provider. First off, notice that we didn't specify a connection string. Your implementation doesn't need to pass in a connection string via the provider declaration in web.config. Your implementation may specify a connection string directly (as we did in Listings 11.6 and 11.7), or it may not need to specify a connection string at all if your provider in turn gets its data from some other data access classes.We also must specify the assembly where we'll find the custom providers. Notice in the add elements for both the membership and roleManager sections that we specify the class name followed by the assembly name in the type attribute. The assembly should of course be compiled and ready in the /bin folder of the application. If the class is not compiled and exists as a class file in the /App_Code folder, you need not specify the assembly.
<?xml version="1.0"?>
<configuration>
<system.web>
<roleManager enabled="true" defaultProvider="CustomRoleProvider">
<providers>
<add name="CustomRoleProvider"
type="MyRoleProvider, MyAssembly" />
</providers>
</roleManager>
<membership defaultProvider="CustomMembershipProvider">
<providers>
<add
name="CustomMembershipProvider"
type="MyMembershipProvider, MyAssembly"
applicationName="MyApplication"
enablePasswordRetrieval="false"
enablePasswordReset="true"
requiresQuestionAndAnswer="true" />
</providers>
</membership>
</system.web>
</configuration>