Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]
.pdf
888C 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
private CreateMembershipFromInternalUser utility method. The provider implementation requires you to implement a couple of methods that work this way. You just need to call the methods of the UserStore appropriately. Some of the methods require you to not return just a MembershipUser but a whole MembershipUserCollection, as follows:
public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
{
try
{
List<SimpleUser> matchingUsers = CurrentStore.Users.FindAll(delegate(SimpleUser user)
{
return user.Email.Equals(emailToMatch, StringComparison.OrdinalIgnoreCase);
});
totalRecords = matchingUsers.Count;
return CreateMembershipCollectionFromInternalList(matchingUsers);
}
catch
{
throw;
}
}
For example, the FindUsersByEmail method finds all users with a specific e-mail (which is possible only if you have configured the provider to not require the e-mail to be unique or if you use pattern matching for e-mails through regular expressions). It returns a collection of Membership users. But as you can see, the method again leverages the FindAll method of the List<> class and an anonymous method for specifying the filter criteria. Therefore, the collection returned from this method is a collection of SimpleUser instances that you use in the back-end store. You can create another helper method for mapping this type of collection to a MembershipUserCollection, as follows:
private MembershipUserCollection CreateMembershipCollectionFromInternalList( List<SimpleUser> users)
{
MembershipUserCollection ReturnCollection = new MembershipUserCollection();
foreach (SimpleUser user in users)
{
ReturnCollection.Add(CreateMembershipFromInternalUser(user));
}
return ReturnCollection;
}
Finally, the LastActivityDate property stored for every user is used by Membership to determine the number of current users online in the application. You have to implement this method in your custom provider through the GetNumberOfUsersOnline method, as follows:
public override int GetNumberOfUsersOnline()
{
int ret = 0;
foreach (SimpleUser user in CurrentStore.Users)
{
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 |
889 |
if (user.LastActivityDate.AddMinutes( Membership.UserIsOnlineTimeWindow) >= DateTime.Now)
{
ret++;
}
}
return ret;
}
This method just goes through all users in the store and uses the UserIsOnlineTimeWindow, which is a property managed through the Membership class and specifies the number of minutes a user is online without any activity. As long as the LastActivityDate with this number of minutes is larger than the current date and time, the user is considered to be online. The LastActivityDate is updated automatically by the different overloads of the GetUser method and the ValidateUser method.
Implementing the remaining functions of the provider does not involve any new concepts, and therefore we will skip them. They merely update some values on users and then call the CurrentStore.Save method to save it to the XML file on the file system. You can download the complete implementation of this provider with the source code for the book.
Implementing the XmlRoleProvider
Implementing the Roles provider is much easier than implementing the Membership provider, because the structures are much simpler for managing roles. Implementing the Roles provider does not introduce any new concepts. It merely requires calling the appropriate methods of the previously introduced RoleStore class for creating roles, deleting roles, assigning users to roles, and deleting users from roles. The complete interface of the Roles provider looks like this:
public class XmlRoleProvider : RoleProvider
{
public override void Initialize(string name, NameValueCollection config)
public override string ApplicationName { get; set; }
public override void CreateRole(string roleName)
public override bool DeleteRole(string roleName, bool throwOnPopulatedRole) public override bool RoleExists(string roleName)
public override void AddUsersToRoles(
string[] usernames, string[] roleNames) public override void RemoveUsersFromRoles(
string[] usernames, string[] roleNames) public override string[] GetAllRoles()
public override string[] GetRolesForUser(string username) public override string[] GetUsersInRole(string roleName)
public override bool IsUserInRole(string username, string roleName) public override string[] FindUsersInRole(
string roleName, string usernameToMatch)
}
As you can see, the class derives from the base class RoleProvider. Again, it overrides the Initialize method for initializing custom properties. But this time initialization of the provider is much simpler because the Roles provider supports only a handful of properties. The only property provided by the base class is the ApplicationName property. Everything else is up to you. Therefore, initialization is fairly simple here:
890C 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 override void Initialize(string name, NameValueCollection config)
{
if (config == null)
{
throw new ArgumentNullException("config");
}
if (string.IsNullOrEmpty(name))
{
name = "XmlRoleProvider";
}
if (string.IsNullOrEmpty(config["description"]))
{
config.Remove("description"); config.Add("description", "XML Role Provider");
}
//Base initialization base.Initialize(name, config);
//Initialize properties
_ApplicationName = "DefaultApp"; foreach (string key in config.Keys)
{
if (key.ToLower().Equals("applicationname")) ApplicationName = config[key];
else if (key.ToLower().Equals("filename")) _FileName = config[key];
}
}
Again, the initialization routine checks the name and description configuration parameters and initializes them with default values if they are not configured. It then calls the base class’s Initialize implementation. Do not forget to call the base class’s Initialize method; otherwise, the default configuration values managed by the base class will not be initialized. Next it initializes the properties while your implementation of the XmlRoleProvider just knows about the ApplicationName and FileName settings. Again, the FileName specifies the name of the XML file where role information is stored.
Next, the class supports a few methods for managing the roles: CreateRole, DeleteRole, and RoleExists. Within these methods, you have to access the underlying RoleStore’s methods, as you can see in this example of CreateRole:
public override void CreateRole(string roleName)
{
try
{
SimpleRole NewRole = new SimpleRole(); NewRole.RoleName = roleName; NewRole.AssignedUsers = new StringCollection();
CurrentStore.Roles.Add(NewRole);
CurrentStore.Save();
}
catch
{
throw;
}
}
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 |
891 |
Compared to the CreateUser method introduced previously, this method is fairly simple. It creates a new instance of SimpleRole and then adds this new role to the underlying RoleStore. Again, you use the CurrentRole property for easy access to the underlying store with the Membership provider’s implementation. You just need to add a property as follows to your class:
private RoleStore CurrentStore
{
get
{
if (_CurrentStore == null)
_CurrentStore = RoleStore.GetStore(_FileName); return _CurrentStore;
}
}
The RoleExists method goes through the CurrentStore.Roles list and verifies whether the role with the name passed in through its parameter exists in the list. The DeleteRole tries to find the role in the roles list of the underlying role store, and if it exists, it deletes the role from the store and then saves the store back to the file system by calling CurrentStore.Save. Most of the methods for your custom Roles provider are that simple. The most complex operations are adding a user to a role and removing the user from the role. The following is the first method—adding users to roles:
public override void AddUsersToRoles(string[] usernames, string[] roleNames)
{
try
{
// Get the roles to be modified foreach (string roleName in roleNames)
{
SimpleRole Role = CurrentStore.GetRole(roleName); if (Role != null)
{
foreach (string userName in usernames)
{
if (!Role.AssignedUsers.Contains(userName))
{
Role.AssignedUsers.Add(userName);
}
}
}
}
CurrentStore.Save();
}
catch
{
throw;
}
}
Although the Roles class you used in Chapter 23 provides more overloads for this type of method, your provider has to implement the most flexible one: adding all users specified in the first parameter array to all roles specified in the second parameter array. Therefore, you have go through the list of supported roles stored in your XML file, and for every role specified in the roleNames parameter you have to add all users specified in the usernames parameter to the corresponding role. That’s what this method is doing. Within the first foreach, it iterates through the array of role names passed in. It retrieves the role from the store by calling the RoleStore’s GetRole method and
892C 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
then adds all the users specified in the usernames parameter to this role. Finally, it calls CurrentStore.Save() for serializing the roles back to the XML file. The RemoveUsersFromRoles is doing the opposite, as follows:
public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
{
try
{
// Get the roles to be modified
List<SimpleRole> TargetRoles = new List<SimpleRole>(); foreach (string roleName in roleNames)
{
SimpleRole Role = CurrentStore.GetRole(roleName); if (Role != null)
{
foreach (string userName in usernames)
{
if (Role.AssignedUsers.Contains(userName))
{
Role.AssignedUsers.Remove(userName);
}
}
}
}
CurrentStore.Save();
}
catch
{
throw;
}
}
The only difference in this method from the one introduced previously is that it removes the users specified in the usernames parameter from all the roles specified in the roleNames parameter. The remaining logic of the method is the same. The remaining methods of the custom Roles provider are easy to implement; in most cases, they just iterate through the roles that exist in the store and return some information, in most cases arrays of strings with user names or role names, as shown here:
public override string[] GetRolesForUser(string username)
{
try
{
List<SimpleRole> RolesForUser = CurrentStore.GetRolesForUser(username); string[] Results = new string[RolesForUser.Count];
for (int i = 0; i < Results.Length; i++) Results[i] = RolesForUser[i].RoleName;
return Results;
}
catch
{
throw;
}
}
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 |
893 |
public override string[] GetUsersInRole(string roleName)
{
try
{
return CurrentStore.GetUsersInRole(roleName);
}
catch
{
throw;
}
}
public override bool IsUserInRole(string username, string roleName)
{
try
{
SimpleRole Role = CurrentStore.GetRole(roleName); if (Role != null)
{
return Role.AssignedUsers.Contains(username);
}
else
{
throw new ProviderException("Role does not exist!");
}
}
catch
{
throw;
}
}
The first method returns all roles for a single user. It therefore calls the RoleStore’s GetRolesForUsers method, which returns a list of SimpleRole classes. The result is then mapped to an array of strings and returned to the caller. Retrieving users for one role is even simpler, as the functionality is provided by the RoleStore class. Finally, the IsUserInRole verifies whether a user is assigned to a role by retrieving the role and then calling the StringCollection’s Contains method to verify whether the user exists in the SimpleRole’s AssignedUsers collection.
You should take a look at one last method—FindUsersInRoles:
public override string[] FindUsersInRole(string roleName, string usernameToMatch)
{
try
{
List<string> Results = new List<string>();
Regex Expression = new Regex(usernameToMatch.Replace("%", @"\w*")); SimpleRole Role = CurrentStore.GetRole(roleName);
if (Role != null)
{
foreach (string userName in Role.AssignedUsers)
{
if (Expression.IsMatch(userName)) Results.Add(userName);
}
}
else
{
894 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
throw new ProviderException("Role does not exist!");
}
return Results.ToArray();
}
catch
{
throw;
}
}
This method tries to find users based on pattern matching in the role specified through the roleName parameter. For this purpose, it retrieves the role from the store and then creates a regular expression. The % character is used by the SQL Membership provider for pattern matching, and because it is a good idea to have a provider that is compatible to existing implementations, you will use it for pattern matching again in your provider. But regular expressions don’t understand the % as a placeholder for any characters in the string; therefore, you need to replace it with a representation that regular expressions understand: \w*. When the Membership class now passes in this character as a placeholder, your pattern matching function will still work, and therefore this function is compatible to the SqlMembershipProvider’s implementation (which also uses the % as a placeholder). The remaining part of the function goes through the users assigned to the role; if the user name matches the pattern, it is added to the resulting list of strings that will be returned as a simple string array.
As you can see, implementing the custom Roles provider is easy if you have previously implemented the custom Membership provider. The process does not require you to understand any new concepts. In general, when you know how to implement one provider, you know how to implement another provider. Therefore, it should be easy for you to implement custom profile and personalization providers. Again, you can download the complete source code for the Roles provider from this book’s website. Now it’s time to discuss how you can use these providers.
Using the Custom Provider Classes
Using providers in a custom web application is fairly easy. The steps for using custom providers are as follows (besides the typical ones such as configuring forms authentication):
1.If you have encapsulated the custom provider in a separate class library (which is definitely useful, as you want to use it in several web applications), you need to add a reference to this class library through the Visual Studio Add References dialog box.
2.Afterward, you must configure the custom provider appropriately in your web.config file.
3.Next you have to select your custom provider as the default provider either through the ASP.NET WAT or through web.config manually.
4.After you have completed these configuration steps, you are ready to use the provider. If you have not added any special functionality and have just implemented the inherited classes straightforwardly as shown in this chapter, you even don’t need to change any code in your application.
The configuration of the previously created XmlMembershipProvider and XmlRoleProvider looks like this:
<membership defaultProvider="XmlMembership"> <providers>
<add name="XmlMembership" applicationName="MyTestApp"
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 |
895 |
fileName="C:\Work\MyTestApp_Users.config" type="Apress.ProAspNet.Providers.XmlMembershipProvider,
Apress.ProAspNet.Providers"
requiresQuestionAndAnswer="true"/>
</providers>
</membership>
<roleManager enabled="true" defaultProvider="XmlRoles">
<providers>
<add name="XmlRoles" applicationName="MyTestApp"
fileName="C:\Work\\MyTestApp_Roles.config" type="Apress.ProAspNet.Providers.XmlRoleProvider,
Apress.ProAspNet.Providers" />
</providers>
</roleManager>
In the previous example, the providers will be configured to use files stored on c:\Work for saving user and role information appropriately. With this configuration, you will find the providers in the ASP.NET WAT (under Providers/Advanced Configuration), as shown in Figure 26-4.
Figure 26-4. Custom providers in the ASP.NET WAT
Don’t try to test the provider; it will fail in this case. Testing providers is just supported for providers that are using database connection strings to connect to the underlying back-end store. Because you are using XML files, testing will not work for the custom provider in this case.
896 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
Debugging Using the WAT
The ASP.NET WAT uses the Membership and Role classes for retrieving and updating data stored through the Membership provider. Although we suggest building your own test driver classes by calling all the methods of the Membership and Role classes, it is definitely useful to have the possibility of debugging from within the ASP.NET WAT, especially if you experience any problems you did not encounter while testing with your own applications.
For debugging through the WAT, you just need to launch the configuration utility through the Website ASP.NET Web Configuration menu and then attach to the web server process hosting the configuration tool. If you are using the file-based web server for development purposes, launch Visual Studio’s Attach to Process dialog box by selecting Debug Attach to Process. Next, find the appropriate web server process. As in most cases, two of these processes will run when using the file-based web server, so you have to attach to the one with the right port number. Match the port number displayed in the address bar of the browser using the ASP.NET WAT with the one displayed in the Attach to Process dialog box. Then your breakpoints in the provider classes will be hit appropriately. Figure 26-5 shows how to attach to the web service process that hosts the ASP.NET WAT.
Figure 26-5. Attaching to the Configuration utility web server process
Summary
In this chapter, you saw how to extend the ASP.NET Membership API and Roles API through custom Membership providers and Roles providers. As an example, you developed a custom, XML-based provider for the Membership and Roles Services. An XML-based provider is appropriate for simple applications only, but you learned the most important concepts for developing a custom Membership and Roles provider. These providers should conform as much as possible to the suggested interfaces so that you don’t have to change your application when using a different provider.
P A R T 5
■ ■ ■
Advanced User Interface
