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.
We'll also need to create a class that inherits from the RoleProvider class, and we'll need to specify that it will manage roles via web.config.
Visual Studio makes the implementation of an abstract class fairly easy by helping you fill in the blanks quickly. Figure 11.2 shows how Intellisense pops up a list of methods when you type "public override."
Note that you don't need to implement every one of these methods, as some are not abstract members. In fact, some are actually from classes further up the inheritance chain (such as the Equals() method, which comes from object). There are a total of 12 methods and five properties to implement, and we'll go through some of them briefly here, with code samples when necessary.
First let's establish the required properties. There are five of them, and all but ApplicationName are read-only. The Name property, if null, should return a name that reflects what your provider does. That makes the property section of our provider look something like Listing 11.4.
C#
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; } } }
VB.NET
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
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.
C#
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; }
VB.NET
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.
C#
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"])); }
VB.NET
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
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.
C#
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(); }
VB.NET
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
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.
C#
MembershipUser user = Membership.GetUser("Jeff"); user.Email = "new@address.com"; Membership.UpdateUser(user);
VB.NET
Dim user As MembershipUser = 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.
<?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>
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.