
Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]
.pdf
868 C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S
Figure 26-1. The Membership and Roles framework
As you can see from their basic architectures, the Membership and Roles Services are independent from each other. Therefore, Membership providers and Role providers have separate base classes; in addition, you can store membership users and roles in different back-end systems. A good example is when using the Roles Service with Windows authentication. Remember what you learned in Chapter 23 about application-specific roles that are used for authorization within the application instead of within Windows groups: this provides you with a way to decouple your application from an underlying Active Directory infrastructure.
Before you learn about the details of implementing custom providers, it’s important to understand why you might want to create a custom Membership provider. Some common reasons include the following:
•You want to use an existing user and roles database that has a different schema than the ASP.NET standard.
•You want to use a database other than Microsoft SQL Server.
•You want to use an unusual data store (such as an XML file, web service, or Active Directory).
•You want to implement additional authentication logic. A good example of this is often implemented for governmental websites where users have to authenticate by specifying three values: a user name, a subscription ID, and a password.
If you just want to store your own information in addition to the information stored by the default implementation, we recommend not implementing a custom provider. Because the Membership API gives you access to a key that uniquely identifies a user in the store, we recommend adding your own tables for storing your additional information and connecting information stored in your tables through the user’s unique key with the actual user of the Membership provider’s storage; alternatively, you could implement user profiles for these additional properties. This is far easier than implementing a custom provider for adding a few extra values.
From within the application, you can access the user’s unique key through the ProviderUserKey property of the MembershipUser class. In this chapter, you will learn how the unique key is propagated to the ProviderUserKey of the MembershipUser class.

C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S |
869 |
Basic Steps for Creating Custom Providers
You will now learn how to implement your custom provider for the Membership and Roles Services. Creating a custom provider involves the following steps:
1.Design and create the underlying data store.
2.Create utility classes for accessing the underlying data store.
3.Create a class that inherits from the MembershipProvider.
4.Create a class that inherits from the RoleProvider.
5.Create a provider test application.
6.Configure the custom providers in your test application.
7.Use the custom providers in your custom application.
Implementing custom providers is fairly straightforward but will require some time, as you have to implement lots of methods and properties. In the following sections, you will create a custom Membership and Roles provider that uses an XML file as the underlying data store. XML files are not a good solution for highly scalable applications but may be a nice alternative if you write a simple application and need to host this application on a provider site and don’t have access to a database such as SQL Server.
Overall Design of the Custom Provider
Before creating a custom provider, you have to think about the overall design of the solution. Your goal is to keep the underlying functionality as simple as possible so that you can concentrate on the actual Membership and Roles provider implementation. In terms of XML, the easiest way to load and save data to XML files is XML serialization. This allows you to store a complete object graph with just one function call in a file and to read it with one function call.
_Serializer = new XmlSerializer(typeof(List<SimpleUser>)); using (XmlTextReader reader = new XmlTextReader(fileName))
{
_Users = (List<SimpleUser>)_Serializer.Deserialize(reader);
}
Because classes such as MembershipUser don’t allow you to access some information—for example, the password—you cannot use them with XML serialization directly; XML serialization requires all properties and members that need to be stored as public properties or members. Therefore, you will create your own representation of users and roles as utility classes for the back-end store. These classes will never be passed to the application, which simply relies on the existing membership classes. (You will include some mapping logic, which is fairly simple, between this internal user representation and the MembershipUser class.) Figure 26-2 shows the overall design of the custom provider solution.
As mentioned, the SimpleUser and SimpleRole classes make XML serialization possible. Although this requires some mapping logic for supporting MembershipUser, this makes the whole implementation much easier. UserStore and RoleStore are both utility classes for encapsulating the access to the XML file. These classes include functions for loading and saving XML files as well as some basic utility functions for searching information in the store.
Finally, the model includes the XmlMembershipProvider and XmlRoleProvider classes. XmlMembershipProvider inherits basic functionality from MembershipProvider, while XmlRoleProvider is inherited from RoleProvider. Both base classes are defined in the System.Web.Security namespace.

870 C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S
Figure 26-2. The design of your custom provider solution
Designing and Implementing the Custom Store
After you have designed your overall architecture, you can start thinking about the underlying data store. In the example, the data store will consist of an XML file for the users and an XML file for the roles. To make access to these files as simple as possible, you will use XML serialization as the primary mechanism for reading from and writing to these files. Therefore, you need some classes to hold the data stored to the XML files either as public fields or as properties, as follows:
public class SimpleUser
{
public Guid UserKey = Guid.Empty;
public string UserName = ""; public string Password = "";
public string Email = "";
public DateTime CreationDate = DateTime.Now;
public DateTime LastActivityDate = DateTime.MinValue; public DateTime LastLoginDate = DateTime.MinValue;
public DateTime LastPasswordChangeDate = DateTime.MinValue; public string PasswordQuestion = "";
public string PasswordAnswer = ""; public string Comment;
}
public class SimpleRole
{
public string RoleName = "";
public StringCollection AssignedUsers = new StringCollection();
}
In this example, you will use a GUID as ProviderUserKey for uniquely identifying users in your store. For every user you will then store a user name, a password (hashed), an e-mail, some date information, a password question and answer, and some comments. For the roles, you will store a name as well as the association to the users. For simplicity, every role will contain an array of user

C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S |
871 |
names (which are strings) that are associated with this role. The serialized version of an array of users will be the user store, while the serialized version of an array of roles will be the roles store, as shown in Figure 26-3.
Figure 26-3. Serialized versions of the SimpleUser/SimpleRole arrays
Another design aspect you have to think about is how to access the store. Basically, for every store, you need only one instance in memory in order to save resources and avoid loading the XML files too often. You can implement this through the Singleton pattern, which is a solution for ensuring that only one instance of a class exists within a process. It does this by making the constructor private and providing a static public method for retrieving an instance. This public method verifies whether the instance already exists, and if not, it automatically creates an instance of its own, which is then returned.
Let’s examine all these aspects based on the UserStore class introduced in Figure 26-3:
private string _FileName; private List<SimpleUser> _Users;
private XmlSerializer _Serializer;
private static Dictionary<string, UserStore> _RegisteredStores;
private UserStore(string fileName)
{
_FileName = fileName;
_Users = new List<SimpleUser>();
_Serializer = new XmlSerializer(typeof(List<SimpleUser>));

872 C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S
LoadStore(_FileName);
}
public static UserStore GetStore(string fileName)
{
//Create the registered store if it does not exist yet if (_RegisteredStores == null)
_RegisteredStores = new Dictionary<string, UserStore>();
//Now return the appropriate store for the filename passed in if (!_RegisteredStores.ContainsKey(fileName))
{
_RegisteredStores.Add(fileName, new UserStore(fileName));
}
return _RegisteredStores[fileName];
}
The class includes a couple of private members for the filename of the store, the list of users, and an XmlSerializer instance used for reading and writing data.
Because the constructor is private, instances can’t be created outside the class. Outside classes can retrieve instances only by calling the public static GetStore() method. The implementation of the Singleton pattern is special in this case. It creates single instances based on the filenames. For every file processed by the provider, one instance of the UserStore class is created. If more than one web application using this provider is running in the same process, you need to ensure that different instances are created for different filenames. Therefore, the class doesn’t manage one static variable for a single instance; instead, it has a dictionary containing all the instances of the class, one for every filename.
Because you are using XML serialization to save and load data to and from the store, the functions for loading the store and saving data back to the store are fairly easy:
private void LoadStore(string fileName)
{
try
{
if (System.IO.File.Exists(fileName))
{
using (XmlTextReader reader = new XmlTextReader(fileName))
{
_Users = (List<SimpleUser>)_Serializer.Deserialize(reader);
}
}
}
catch (Exception ex)
{
throw new Exception(
string.Format("Unable to load file {0}", fileName), ex);
}
}
private void SaveStore(string fileName)
{
try
{
if (System.IO.File.Exists(fileName)) System.IO.File.Delete(fileName);

C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S |
873 |
using (XmlTextWriter writer =
new XmlTextWriter(fileName, Encoding.UTF8))
{
_Serializer.Serialize(writer, _Users);
}
}
catch (Exception ex)
{
throw new Exception(
string.Format("Unable to save file {0}", fileName), ex);
}
}
Both functions are private, as they are called only within the class itself. The LoadStore() method is called within the constructor of the UserStore class. Within the method, the private variable _Users is initialized. Every subsequent query happens based on querying the _Users collection of the store class. The SaveStore() method, on the other hand, just serializes the _Users collection to the file specified in the private _FileName member, which is passed in through the constructor (and indirectly through the static GetStore() method). Finally, the class supports a couple of methods for querying information in the _Users collection.
public List<SimpleUser> Users
{
get { return _Users; }
}
public void Save()
{
SaveStore(_FileName);
}
public SimpleUser GetUserByName(string name)
{
return _Users.Find(delegate(SimpleUser user)
{
return string.Equals(name, user.UserName);
});
}
public SimpleUser GetUserByEmail(string email)
{
return _Users.Find(delegate(SimpleUser user)
{
return string.Equals(email, user.Email);
});
}
public SimpleUser GetUserByKey(Guid key)
{
return _Users.Find(delegate(SimpleUser user)
{
return (user.UserKey.CompareTo(key) == 0);
});
}
The Users property is a simple property that allows the actual provider (XmlMembershipProvider) to access users of the store. After the provider implementation has changed something

874C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S
within the store (has changed properties of a user, for example), it calls the public Save() method, which internally calls the SaveStore() to serialize information back to the file specified in the private _FileName variable of this instance. The remaining methods are for searching users based on different criteria. For this purpose, the generic List<> includes a find method. This find method accepts a reference to another method that is called for every element while iterating through the list for comparison. If the comparison function returns true for an element, the element is included in
the results.
public SimpleUser GetUserByKey(Guid key)
{
return _Users.Find(delegate(SimpleUser user)
{
return (user.UserKey.CompareTo(key) == 0);
});
}
In this code, you pass in a delegate (which is a reference to a function) that compares the internal SimpleUser’s key with the key passed in. If this is true, the current user that is passed in as a parameter from the List<> is returned as a result; otherwise, the List<> continues iterating through its elements. The inline implementation of the method, without explicitly creating a method with a separate prototype, is called an anonymous method and is a special feature of C# for saving additional code for short algorithm parameters.
The UserStore includes the implementation for saving user information only. Roles are not included. For this purpose, you have to implement the RoleStore class (which is similar to the UserStore class), as shown here:
public class RoleStore
{
XmlSerializer _Serializer; private string _FileName; List<SimpleRole> _Roles;
#region "Singleton Implementation"
private static Dictionary<string, RoleStore> _RegisteredStores;
public static RoleStore GetStore(string fileName)
{
//Create the registered stores if (_RegisteredStores == null)
_RegisteredStores = new Dictionary<string, RoleStore>();
//Now return the appropriate store
if (!_RegisteredStores.ContainsKey(fileName))
{
_RegisteredStores.Add(fileName, new RoleStore(fileName));
}
return _RegisteredStores[fileName];
}
private RoleStore(string fileName)
{
_Roles = new List<SimpleRole>(); _FileName = fileName;
_Serializer = new XmlSerializer(typeof(List<SimpleRole>));

C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S |
875 |
LoadStore(_FileName);
}
#endregion
#region "Private Helper Methods"
private void LoadStore(string fileName)
{
try
{
if (System.IO.File.Exists(fileName))
{
using (XmlTextReader reader = new XmlTextReader(fileName))
{
_Roles = (List<SimpleRole>)_Serializer.Deserialize(reader);
}
}
}
catch (Exception ex)
{
throw new Exception(string.Format(
"Unable to load file {0}", fileName), ex);
}
}
private void SaveStore(string fileName)
{
try
{
if (System.IO.File.Exists(fileName)) System.IO.File.Delete(fileName);
using (XmlTextWriter writer =
new XmlTextWriter(fileName, Encoding.UTF8))
{
_Serializer.Serialize(writer, _Roles);
}
}
catch (Exception ex)
{
throw new Exception(string.Format(
"Unable to save file {0}", fileName), ex);
}
}
#endregion
public List<SimpleRole> Roles
{
get { return _Roles; }
}
public void Save()
{
SaveStore(_FileName);
}

876 C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S
public List<SimpleRole> GetRolesForUser(string userName)
{
List<SimpleRole> Results = new List<SimpleRole>(); foreach (SimpleRole r in Roles)
{
if (r.AssignedUsers.Contains(userName)) Results.Add(r);
}
return Results;
}
public string[] GetUsersInRole(string roleName)
{
SimpleRole Role = GetRole(roleName); if (Role != null)
{
string[] Results = new string[Role.AssignedUsers.Count]; Role.AssignedUsers.CopyTo(Results, 0);
return Results;
}
else
{
throw new Exception(string.Format(
"Role with name {0} does not exist!", roleName));
}
}
public SimpleRole GetRole(string roleName)
{
return Roles.Find(delegate(SimpleRole role)
{
return role.RoleName.Equals(
roleName, StringComparison.OrdinalIgnoreCase);
});
}
}
This implementation looks fairly similar to the UserStore. The major difference is that it uses the SimpleRole class instead of the SimpleUser class, and it initializes the XmlSerializer class with a different type. Also, the functions for querying the store are different.
Now the classes for accessing the underlying stores are complete, which means you can start implementing the custom provider classes.
Implementing the Provider Classes
In this section, you will create the XmlMembershipProvider class, which actually fulfills the role of an adapter between your custom store and the requirements of the Membership API. (The code for the complete provider implementation is included in this book’s downloads.) In this section you will go through the most important parts of creating a Membership provider.
Every custom Membership provider must be inherited from System.Web.Security.Membership provider, as follows:
public class XmlMembershipProvider : MembershipProvider
{
// ...
}

C H A P T E R 2 6 ■ C U S TO M M E M B E R S H I P P R OV I D E R S |
877 |
When inheriting from MembershipProvider, you have to implement lots of properties and methods to fulfill the requirements of the Membership API. These properties and methods are used for querying, creating, updating, and deleting users as well as retrieving specific information about the provider such as password requirements. These types of properties are queried by the security controls introduced in Chapter 21. (For example, the RequirePasswordQuestionAndAnswer property is queried by the CreateUserWizard to decide whether to display the text boxes for entering password questions and answers.) You should start by implementing the properties of the provider, as this is the easiest part of the whole task. For every property, you should provide one private variable that contains the state of the appropriate property.
public override string ApplicationName { } public override bool EnablePasswordReset { } public override bool EnablePasswordRetrieval { }
public override int MaxInvalidPasswordAttempts { }
public override int MinRequiredNonAlphanumericCharacters { } public override int MinRequiredPasswordLength { }
public override int PasswordAttemptWindow { }
public override MembershipPasswordFormat PasswordFormat { } public override string PasswordStrengthRegularExpression { } public override bool RequiresQuestionAndAnswer { }
public override bool RequiresUniqueEmail { }
For a detailed description of these properties, you can refer to Chapter 21. The properties of providers are described there, and they have the same meaning as in the underlying provider implementation. Many of these properties just have get accessors and no setters. So, how can the ASP.NET infrastructure initialize these properties with values configured in web.config? You can find the answer in the original base class for all providers, which is in the System.Configuration.Provider.ProviderBase class. The ProviderBase class in turn is the base class for MembershipProvider class, and therefore all classes that inherit from MembershipProvider are indirectly inherited from ProviderBase and have the basic properties of ProviderBase. All you have to do is override the Initialize method. This method accepts two parameters: a name (which is configured through the name attribute in web.config) and a NameValueCollection (which contains keys and their appropriate values for all settings configured through web.config). Within this method you can initialize the private members of the properties shown previously.
Let’s examine the contents of this function for the XmlMembershipProvider step by step:
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
if (config == null)
{
throw new ArgumentNullException("config");
}
if (string.IsNullOrEmpty(name))
{
name = "XmlMembershipProvider";
}
if (string.IsNullOrEmpty(config["description"]))
{
config.Remove("description"); config.Add("description", "XML Membership Provider");
}
// Initialize the base class base.Initialize(name, config);
...