Chapter 15

Extending the Provider Model

WHAT’S IN THIS CHAPTER?

WROX.COM CODE DOWNLOADS FOR THIS CHAPTER

Please note that all the code examples in this chapter are available as a part of this chapter’s code download on the book’s website at www.wrox.com on the Download Code tab.

The previous chapter introduced the provider model found in ASP.NET 4.5 and explained how it is used with the membership and role management systems.

As discussed in the previous chapter, these systems in ASP.NET 4.5 require that some type of user state be maintained for long periods of time. Their time-interval and security requirements for state storage are greater than those for earlier systems that simply used the Session object. Out of the box, ASP .NET 4.5 gives you a series of providers to use as the underlying connectors for any data storage needs that arise from state management for these systems.

The providers that come with the default install of the .NET Framework 4.5 include the most common means of state management data storage needed to work with any of the systems. But like most things in .NET, you can customize and extend the providers that are supplied.

This chapter looks at some of the ways to extend the provider model found in ASP.NET 4.5. This chapter also reviews a couple of sample extensions to the provider model. First, however, you look at some of the simpler ways to modify and extend the providers already present in the default install of .NET 4.5.

PROVIDERS ARE ONE TIER IN A LARGER ARCHITECTURE

Remember from the previous chapter that providers enable you to define the data-access tier for many of the systems in ASP.NET 4.5. They also enable you to define your core business logic implementation on how the data is manipulated or handled. They enable you to use the various controls and APIs that compose these systems in a uniform manner regardless of the underlying data storage method of the provider. The provider model also enables you to easily swap one provider for another without affecting the underlying controls and API that are interacting with the provider. Figure 15-1 presents this model.

FIGURE 15-1

image

From this diagram, you can see that both the controls utilized in the membership system, as well as the Membership API, use the defined provider. Changing the underlying provider does not change the controls or the API, but you can definitely modify how these items behave (as you see shortly). You can also simply change the location where the state management required by these items is stored.


NOTE Besides the samples covered in this chapter, you can use the ASP.NET Universal Providers (by installing the Microsoft.AspNet.Providers package in Package Manager). The Universal Providers do not alter the API, but they do replace the data store mechanism from the built-in providers. As mentioned in the previous chapter, you can find more on this topic in Chapters 18 and 19.

MODIFYING THROUGH ATTRIBUTE-BASED PROGRAMMING

Probably the easiest way to modify the behaviors of the providers built into the .NET Framework 4.5 is through attribute-based programming. In ASP.NET 4.5, you can apply advanced behavior modification through attribute usage. Using the definitions of the providers found in either the machine.config files or within the root web.config file, you can change the provider behavior. This chapter provides an example of how to modify the SqlMembershipProvider.

Simpler Password Structures through the SqlMembershipProvider

When you create users with the SqlMembershipProvider instance, whether you are using SQL Server Express or Microsoft’s SQL Server 2005, 2008, or 2012, notice that the password required to create a user is a “semi-strong” password. This is evident when you create a user through the ASP.NET Web Site Administration Tool, as illustrated in Figure 15-2. For more about the ASP.NET Web Site Administration Tool, check out Appendix D.

FIGURE 15-2

image

On this screen, I attempted to enter a password and was notified that the password did not meet the application’s requirements. Instead, I was warned that the minimum password length is seven characters and that at least one non-alphanumeric character is required. This means that a password such as Micro$oft is what is required. This kind of behavior is specified by the membership provider and not by the controls or the API used in the membership system. You find the definition of the requirements in the machine.config.comments file located at C:\WINDOWS\Microsoft.NET\Framework\v4.0.xxxxx\CONFIG. Listing 15-1 presents this definition.

LISTING 15-1: The SqlMembershipProvider instance declaration

<?xml version="1.0"?>
<configuration>
    <system.web>
        <membership defaultProvider="AspNetSqlMembershipProvider" 
                    userIsOnlineTimeWindow="15" hashAlgorithmType="">
            <providers>
                <clear />
                <add connectionStringName="LocalSqlServer" 
                     enablePasswordRetrieval="false" 
                     enablePasswordReset="true" requiresQuestionAndAnswer="true" 
                     applicationName="/" requiresUniqueEmail="false" 
                     passwordFormat="Hashed" maxInvalidPasswordAttempts="5" 
                     minRequiredPasswordLength="7" 
                     minRequiredNonalphanumericCharacters="1" 
                     passwordAttemptWindow="10" 
                     passwordStrengthRegularExpression="" 
                     name="AspNetSqlMembershipProvider" 
                     type="System.Web.Security.SqlMembershipProvider, System.Web, 
                         Version=4.0.0.0, Culture=neutral, 
                         PublicKeyToken=b03f5f7f11d50a3a" />
            </providers>
        </membership>
    </system.web>
</configuration>

Looking over the attributes of this provider, notice that the minRequiredPasswordLength and the minRequiredNonalphanumericCharacters attributes define this behavior. To change this behavior across every application on the server, you simply change these values in this file. However, we suggest simply changing these values in your application’s web.config file, as shown in Listing 15-2. (Since the web.config changes multiple times, this file is saved as Listing15-02.xml in the code download for this chapter for easier downloading.)

LISTING 15-2: Changing attribute values in the web.config file

<?xml version="1.0" ?>
<configuration>
    <system.web>
        <authentication mode="Forms" />
        <membership>
            <providers>
                <clear />
                <add connectionStringName="LocalSqlServer" 
                     enablePasswordRetrieval="false" 
                     enablePasswordReset="true" 
                     requiresQuestionAndAnswer="true" 
                     applicationName="/" requiresUniqueEmail="false" 
                     passwordFormat="Hashed" maxInvalidPasswordAttempts="5" 
                     minRequiredPasswordLength="4" 
                     minRequiredNonalphanumericCharacters="0" 
                     passwordAttemptWindow="10" 
                     name="AspNetSqlMembershipProvider" 
                     type="System.Web.Security.SqlMembershipProvider, 
                         System.Web, Version=4.0.0.0, Culture=neutral, 
                         PublicKeyToken=b03f5f7f11d50a3a" />
            </providers>
        </membership>
    </system.web>
</configuration>

In this example, the password requirements are changed through the minRequiredPasswordLength and minRequiredNonalphanumericCharacters attributes. In this case, the minimum length allowed for a password is four characters, and none of those characters is required to be non-alphanumeric (for example, a special character such as !, $, or #).

Redefining a provider in the application’s web.config file is a fairly simple process. In the example in Listing 15-2, you can see that the <membership> element is quite similar to the same element presented in the machine.config file.

You have a couple of options when defining your own instance of the SqlMembershipProvider. One approach, as presented in Listing 15-2, is to redefine the named instance of the SqlMembershipProvider that is defined in the machine.config file (AspNetSqlMembershipProvider, the value from the name attribute in the provider declaration). If you take this approach, you must clear the previous defined instance of AspNetSqlMembershipProvider. You must redefine the AspNetSqlMembershipProvider using the <clear /> node within the <providers> section. Failure to do so causes an error to be thrown stating that this provider name is already defined.

After you have cleared the previous instance of AspNetSqlMembershipProvider, you redefine this provider using the <add> element. In the case of Listing 15-2, you can see that the password requirements are redefined with the use of new values for the minRequiredPasswordLength and the minRequiredNonalphanumericCharacters attributes (shown in bold).

The other approach to defining your own instance of the SqlMembershipProvider is to give the provider defined in the <add> element a unique value for the name attribute. If you take this approach, you must specify this new named instance as the default provider of the membership system using the defaultProvider attribute. Listing 15-3 presents this approach. (Since the web.config changes multiple times, this file is saved as Listing15-03.xml in the code download for this chapter for easier downloading.)

LISTING 15-3: Defining your own named instance of the SqlMembershipProvider

<?xml version="1.0"?>
<configuration>
    <system.web>
        <authentication mode="Forms" />
        <membership defaultProvider="JasonsSqlMembershipProvider">
            <providers>
                <add connectionStringName="LocalSqlServer"
                     enablePasswordRetrieval="false"
                     enablePasswordReset="true"
                     requiresQuestionAndAnswer="true"
                     applicationName="/" requiresUniqueEmail="false"
                     passwordFormat="Hashed" maxInvalidPasswordAttempts="5"
                     minRequiredPasswordLength="4"
                     minRequiredNonalphanumericCharacters="0"
                     passwordAttemptWindow="10"
                     name="JasonsSqlMembershipProvider"
                     type="System.Web.Security.SqlMembershipProvider, 
                         System.Web, Version=4.0.0.0, Culture=neutral, 
                         PublicKeyToken=b03f5f7f11d50a3a" />
            </providers>
        </membership>
    </system.web>
</configuration>

In this case, the SqlMembershipProvider instance in the machine.config file (defined under the JasonsSqlMembershipProvider name) is not even redefined. Instead, a completely new named instance (JasonsSqlMembershipProvider) is defined here in the web.config file.

Stronger Password Structures through the SqlMembershipProvider

Next, let’s make the password structure a bit more complicated. You can, of course, accomplish this task in a couple of ways. One approach is to use the same minRequiredPasswordLength and minRequiredNonalphanumericCharacters attributes (as shown earlier) to make the password meet a required length (longer passwords usually mean more secure passwords) and to make the password contain a certain number of non-alphanumeric characters (which also makes for a more secure password).

Another option is to use the passwordStrengthRegularExpression attribute. If the minRequiredPasswordLength and the minRequiredNonalphanumericCharacters attributes cannot give you the password structure you are searching for, using the passwordStrengthRegularExpression attribute is your next best alternative.

For an example of using this attribute, suppose you require that the user’s password contains the following:

You can then define your provider as shown in Listing 15-4. (Since the web.config changes multiple times, this file is saved as Listing15-04.xml in the code download for this chapter for easier downloading.)

LISTING 15-4: A provider instance in the web.config file to change the password structure

<?xml version="1.0"?>
<configuration>
    <system.web>
        <authentication mode="Forms" />
        <membership defaultProvider="JasonsSqlMembershipProvider">
            <providers>
                <add connectionStringName="LocalSqlServer"
                     enablePasswordRetrieval="false"
                     enablePasswordReset="true"
                     requiresQuestionAndAnswer="true"
                     applicationName="/" requiresUniqueEmail="false"
                     passwordFormat="Hashed" maxInvalidPasswordAttempts="5"
                     passwordStrengthRegularExpression=
                         "(?=^.{8,}$)(?=.*\d)(?=.*\W+)(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$"
                     passwordAttemptWindow="10"
                     name="JasonsSqlMembershipProvider"
                     type="System.Web.Security.SqlMembershipProvider, 
                         System.Web, Version=4.0.0.0, Culture=neutral, 
                         PublicKeyToken=b03f5f7f11d50a3a" />
            </providers>
        </membership>
    </system.web>
</configuration>

Instead of using the minRequiredPasswordLength and the minRequiredNonalphanumericCharacters attributes, the passwordStrengthRegularExpression attribute is used and given a value of (?=^.{8,}$)(?=.*\d)(?=.*\W+)(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$.


NOTE You can find several online resources for discovering regular expressions. As mentioned in Chapter 6, one of the most popular Internet sites for finding regular expressions is called RegExLib, found at www.regexlib.com.

The lesson here is that you have many ways to modify the behaviors of the providers already available in the .NET Framework 4.5 install. You can adapt a number of providers built into the framework to suit your needs by using attribute-based programming. The SqlMembershipProvider example demonstrated this technique, and you can just as easily make similar types of modifications to any of the other providers.

EXAMINING PROVIDERBASE

All the providers derive in some fashion from the ProviderBase class, found in the System.Configuration.Provider namespace. ProviderBase is an abstract class used to define a base template for inheriting providers. Looking at ProviderBase, note that there isn’t much to this abstract class, as illustrated in Figure 15-3.

FIGURE 15-3

image

As stated, there is not much to this class. It is really just a root class for a provider that exists to allow providers to initialize themselves.

The Name property is used to provide a friendly name, such as AspNetSqlRoleProvider. The Description property is used to enable a textual description of the provider, which can then be used later by any administration tools. The main item in the ProviderBase class is the Initialize() method. Here is the constructor for Initialize():

public virtual void Initialize(string name, NameValueCollection config)

Note the two parameters to the Initialize() method. The first is the name parameter, which is simply the value assigned to the name attribute in the provider declaration in the configuration file. The config parameter is of type NameValueCollection, which is a collection of name and value pairs. These name/value pairs are the items that are also defined in the provider declaration in the configuration file as all the various attributes and their associated values.

When looking over the providers that are included in the default install of ASP.NET 4.5, note that each of the providers has defined a class you can derive from that implements the ProviderBase abstract class. For example, looking at the model in place for the membership system, you can see a base MembershipProvider instance that is inherited in the final SqlMembershipProvider declaration. The MembershipProvider, however, implements the ProviderBase itself. Figure 15-4 presents this model.

FIGURE 15-4

image

Notice that each of the various systems has a specific base provider implementation for you to work with. There really cannot be a single provider that addresses the needs of all the available systems. Looking at Figure 15-4, you can see that the MembershipProvider instance exposes some very specific functionality required by the ASP.NET membership system. The methods exposed are definitely not needed by the role management system or the Web Parts capability.

With these various base implementations in place, when you are creating your own customizations for working with the ASP.NET membership system, you have several options available to you. First, you can simply create your own provider that implements the ProviderBase. We do not recommend this approach, however, because abstract classes are already in place for you to use with the various systems. Therefore, as mentioned, you can implement the MembershipProvider instance (a better approach) and work from the model it provides. Finally, if you are working with SQL Server in some capacity and simply want to change the underlying behaviors of this provider, you can inherit from SqlMembershipProvider and modify the behavior of the class from this inheritance. The next section evaluates the various means of extending the provider model through examples.

BUILDING YOUR OWN PROVIDERS

You now examine the process of building your own provider to use within your ASP.NET application. Actually, providers are not that difficult to put together (as you see shortly) and can even be created directly in any of your ASP.NET 4.5 projects. The example demonstrates building a membership provider that works from an XML file. For a smaller website, this scenario might be common. For larger websites and web-based applications, you probably want to use a database of some kind, rather than an XML file, for managing users.

You have a couple of options when building your own membership provider. You can derive from a couple of classes — the SqlMembershipProvider class or the MembershipProvider class — to build the functionality you need. You derive from the SqlMembershipProvider class only if you want to extend or change the behavior of the membership system as it interacts with SQL. Because the goal here is to build a read-only XML membership provider, deriving from this class is inappropriate. In this case, basing everything on the MembershipProvider class is best.

Creating the CustomProviders Application

In this section, you create a new website project called CustomProviders in the language of your choice. For this example, you want to build the new membership provider directly in the web application itself. Another option is to build the provider in a Class Library project and then to reference the generated DLL in your web project. Either way is fine in the end.


NOTE For the examples in this chapter, we have created Chapter15-CustomProviders-VB and Chapter15-CustomProviders-CS, respectively, for the code download available at www.wrox.com.

Because you are going to build this provider directly in the website project itself, you create the App_Code folder in your application. This location is where you want to place the class file that you create. The class file is the actual provider in this case.

After the App_Code folder is in place, create a new class in this folder and call the class either XmlMembershipProvider.vb or XmlMembershipProvider.cs, depending on the language you are using. With this class now in place, have your new XmlMembershipProvider class derive from MembershipProvider. To accomplish this task and to know which methods and properties to override, you can use Visual Studio 2012 to build a skeleton of the class you want to create. You can step through this process starting with the code demonstrated in Listing 15-5.

LISTING 15-5: The start of your XmlMembershipProvider class

VB

Public Class XmlMembershipProvider
    Inherits MembershipProvider
End Class

C#

namespace Chapter15_CustomProviders_CS.App_Code
{
    public class XmlMembershipProvider : MembershipProvider
    {
    }
}

To start, you’ll make one change to this new class, the XmlMembershipProvider class. You’ll update it to inherit from MembershipProvider.

Constructing the Class Skeleton Required

To get Visual Studio 2012 to stub out your class with the appropriate methods and properties, take the following steps (depending on the language you are using). If you are using Visual Basic, all you have to do is press the Enter key. In C#, all you have to do is right-click the MembershipProvider statement in your code and simply select Implement Abstract Class from the available options. Another option is to place the cursor on the MembershipProvider statement in the document window and then select Edit ⇒ IntelliSense ⇒ Implement Abstract Class from the Visual Studio menu. After you perform one of these operations, you see the full skeleton of the class in the document window of Visual Studio. Listing 15-6 shows the code that is generated if you are creating a C# XmlMembershipProvider class (you’ll soon see why we are only showing the C# example here).

LISTING 15-6: Code generated for the XmlMembershipProvider class by Visual Studio

C#

namespace Chapter15_CustomProviders_CS.App_Code
{
    public class XmlMembershipProvider : MembershipProvider
    {
        public override string ApplicationName
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }
 
        public override bool ChangePassword(string username, string oldPassword, 
          string newPassword)
        {
            throw new NotImplementedException();
        }
 
        public override bool ChangePasswordQuestionAndAnswer(string username, string password, 
          string newPasswordQuestion, string newPasswordAnswer)
        {
            throw new NotImplementedException();
        }
 
        public override MembershipUser CreateUser(string username, string password, 
          string email, string passwordQuestion, string passwordAnswer, bool isApproved, 
          object providerUserKey, out MembershipCreateStatus status)
        {
            throw new NotImplementedException();
        }
 
        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            throw new NotImplementedException();
        }
 
        public override bool EnablePasswordReset
        {
            get { throw new NotImplementedException(); }
        }
 
        public override bool EnablePasswordRetrieval
        {
            get { throw new NotImplementedException(); }
        }
 
        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, 
          int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }
 
        public override MembershipUserCollection FindUsersByName(string usernameToMatch, 
          int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }
 
        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, 
          out int totalRecords)
        {
            throw new NotImplementedException();
        }
 
        public override int GetNumberOfUsersOnline()
        {
            throw new NotImplementedException();
        }
 
        public override string GetPassword(string username, string answer)
        {
            throw new NotImplementedException();
        }
 
        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            throw new NotImplementedException();
        }
 
        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            throw new NotImplementedException();
        }
 
        public override string GetUserNameByEmail(string email)
        {
            throw new NotImplementedException();
        }
 
        public override int MaxInvalidPasswordAttempts
        {
            get { throw new NotImplementedException(); }
        }
 
        public override int MinRequiredNonAlphanumericCharacters
        {
            get { throw new NotImplementedException(); }
        }
 
        public override int MinRequiredPasswordLength
        {
            get { throw new NotImplementedException(); }
        }
 
        public override int PasswordAttemptWindow
        {
            get { throw new NotImplementedException(); }
        }
 
        public override MembershipPasswordFormat PasswordFormat
        {
            get { throw new NotImplementedException(); }
        }
 
        public override string PasswordStrengthRegularExpression
        {
            get { throw new NotImplementedException(); }
        }
 
        public override bool RequiresQuestionAndAnswer
        {
            get { throw new NotImplementedException(); }
        }
 
        public override bool RequiresUniqueEmail
        {
            get { throw new NotImplementedException(); }
        }
 
        public override string ResetPassword(string username, string answer)
        {
            throw new NotImplementedException();
        }
 
        public override bool UnlockUser(string userName)
        {
            throw new NotImplementedException();
        }
 
        public override void UpdateUser(MembershipUser user)
        {
            throw new NotImplementedException();
        }
 
        public override bool ValidateUser(string username, string password)
        {
            throw new NotImplementedException();
        }
    }
}

Wow, that’s a lot of code! Although the skeleton is in place, the next step is to code some of the items that will be utilized by the provider that Visual Studio laid out for you — starting with the XML file that holds all the users allowed to access the application.

Creating the XML User Data Store

Because this provider is an XML membership provider, the intent is to read the user information from an XML file rather than from a database such as SQL Server. For this reason, you must define the XML file structure that the provider can use. Listing 15-7 shows the structure used for this example (App_Data\Users.xml in each of the respective projects).

LISTING 15-7: The XML file used to store usernames and passwords

<?xml version="1.0" encoding="utf-8" ?>
<Users>
    <User>
        <Username>JasonGaylord</Username>
        <Password>Reindeer</Password>
        <Email>jason@jasongaylord.com</Email>
        <DateCreated>12/10/2012</DateCreated>
    </User>
    <User>
        <Username>ScottHanselman</Username>
        <Password>YabbaDabbaDo</Password>
        <Email>scott@outlook.com</Email>
        <DateCreated>12/02/2012</DateCreated>
    </User>
    <User>
        <Username>ChristianWenz</Username>
        <Password>BamBam</Password>
        <Email>christian@outlook.com</Email>
        <DateCreated>01/11/2013</DateCreated>
    </User>
</Users>

This XML file holds three user instances, all of which include the username, password, e-mail address, and the date on which the user is created. Because it is a data file, you should place this file in the App_Data folder of your ASP.NET application. You can name the file anything you want; but in this case, we have named the file Users.xml.

Later, this chapter reviews how to grab these values from the XML file when validating users.

Defining the Provider Instance in the web.config File

As you saw in the previous chapter on providers, you define a provider and its behavior in a configuration file (such as the machine.config or the web.config file). Because this provider is being built for a single application instance, this example defines the provider in the web.config file of the application.

The default provider is the SqlMembershipProvider, which is defined in the machine.config file on the server. For this example, you must override this setting and establish a new default provider. The XML membership provider declaration in the web.config should appear as shown in Listing 15-8.

LISTING 15-8: Defining the XmlMembershipProvider in the web.config file

<?xml version="1.0"?>
<configuration>
    <system.web>
        <compilation debug="true" targetFramework="4.5" />
        <httpRuntime targetFramework="4.5" />
        <authentication mode="Forms"/>
        <membership defaultProvider="XmlFileProvider">
            <providers>
                <add name="XmlFileProvider" type="XmlMembershipProvider"
                 xmlUserDatabaseFile="/App_Data/Users.xml"/>
            </providers>
        </membership>
    </system.web>
</configuration>

In this listing, you can see that the default provider is defined as the XmlFileProvider. Because this provider name will not be found in any of the parent configuration files, you must define XmlFileProvider in the web.config file.

Using the defaultProvider attribute, you can define the name of the provider you want to use for the membership system. In this case, it is XmlFileProvider. Then you define the XmlFileProvider instance using the <add> element within the <providers> section. The <add> element gives a name for the provider — XmlFileProvider. It also points to the class (or type) of the provider. In this case, it is the skeleton class you just created — XmlMembershipProvider.


NOTE The provider type may need a fully qualified class name, meaning that the class name is preceded with the appropriate namespace.

Beyond the attributes already used so far, you can create any attribute in your provider declaration that you want. Whatever type of provider you create, however, you must address the attributes in your provider and act upon the values that are provided with the attributes. In the case of the simple XmlMembershipProvider, only a single custom attribute exists — xmlUserDatabaseFile. This attribute points to the location of the user database XML file. For this provider, it is an optional attribute. If you do not provide a value for xmlUserDatabaseFile, you have a default value. In Listing 15-8, however, you can see that a value is indeed provided for the XML file to use. Note that the xmlUserDatabaseFile is simply the filename.

One attribute is not shown in the example, but is an allowable attribute because it is addressed in the XmlMembershipProvider’s class. This attribute, the applicationName attribute, points to the application that the XmlMembershipProvider instance should address. Here is the default value, which you can also place in this provider declaration within the configuration file:

applicationName="/"

Not Implementing Methods and Properties of the MembershipProvider Class

Now, turn your attention to the XmlMembershipProvider class. The next step is to implement any methods or properties needed by the provider. You are not required to make any real use of the methods contained in this skeleton; instead, you can simply build-out only the methods you are interested in working with. For example, if you do not allow for programmatic access to change passwords (and, in turn, the controls that use this programmatic access), you either want to not initiate an action or to throw an exception if someone tries to implement this method, as shown in Listing 15-9.

LISTING 15-9: Not implementing one of the available methods by throwing an exception

VB

Public Overrides Function ChangePassword(username As String, 
    oldPassword As String, newPassword As String) As Boolean
    Throw New NotSupportedException()
End Function

C#

public override bool ChangePassword(string username, string oldPassword, 
    string newPassword)
{
    throw new NotSupportedException();
}

In this case, a NotSupportedException is thrown if the ChangePassword() method is invoked. If you do not want to throw an actual exception, you can simply return a false value and not take any other action, as shown in Listing 15-10 (although not throwing an exception may cause annoyance for a developer who is trying to implement this provider).

LISTING 15-10: Not implementing one of the available methods by returning a false value

VB

Public Overrides Function ChangePassword(username As String, 
    oldPassword As String, newPassword As String) As Boolean
    Return False
End Function

C#

public override bool ChangePassword(string username, string oldPassword, 
    string newPassword)
{
    return false;
}

This chapter does not address every possible action you can take with XmlMembershipProvider, and therefore, you may want to work through the available methods and properties of the derived MembershipProvider instance and make the necessary changes to any item that you won’t be using.

Implementing Methods and Properties of the MembershipProvider Class

Now it is time to implement some of the methods and properties available from the MembershipProvider class to get the XmlMembershipProvider class to work. The first items are some private variables that multiple methods can utilize throughout the class. Listing 15-11 presents these variable declarations.

LISTING 15-11: Declaring some private variables in the XmlMembershipProvider class

VB

Public Class XmlMembershipProvider
    Inherits MembershipProvider
 
    Private _AppName As String
    Private _MyUsers As Dictionary(Of String, MembershipUser)
    Private _FileName As String
 
    ' Code removed for clarity
End Class

C#

public class XmlMembershipProvider : MembershipProvider
{
    private string _AppName;
    private Dictionary<string, MembershipUser> _MyUsers;
    private string _FileName;
        
    // Code removed for clarity
}

The variables being declared are items needed by multiple methods in the class. The _AppName variable defined will be set to the application name that uses the XML membership provider. In all cases, it is the local application. You also want to place all the members found in the XML file into a collection of some type. This example uses a dictionary generic type named _MyUsers. Finally, this example points to the file to use with the _FileName variable.

Defining the ApplicationName Property

After the private variables are in place, the next step is to define the ApplicationName property. You now make use of the first private variable — _AppName. Listing 15-12 presents the property definition of ApplicationName.

LISTING 15-12: Defining the ApplicationName property

VB

Public Overrides Property ApplicationName() As String
    Get
        Return _AppName
    End Get
    Set(ByVal value As String)
        _AppName = value
    End Set
End Property

C#

public override string ApplicationName
{
    get
    {
        return _AppName;
    }
    set
    {
        _AppName = value;
    }
}

Now that the ApplicationName property is defined and in place, the next step is to retrieve the values defined in the web.config file’s provider declaration (XmlFileProvider).

Extending the Initialize() Method

You now extend the Initialize() method so that it reads in the custom attribute and its associated values as defined in the provider declaration in the web.config file. Look through the class skeleton of your XmlMembershipProvider class, and note that no Initialize() method is included in the list of available items.

The Initialize() method is invoked when the provider is first initialized. Overriding this method is not a requirement, and therefore, you won’t see it in the declaration of the class skeleton. To put the Initialize() method in place within the XmlMembershipProvider class, simply type Public Overrides (for Visual Basic) or public override (for C#) in the class. IntelliSense then presents you with the Initialize() method, as shown in Figure 15-5.

FIGURE 15-5

image

Placing the Initialize() method in your class in this manner is quite easy. Select the Initialize() method from the list in IntelliSense and press the Enter key. This method gives you a base construction of the method in your code, as shown in Listing 15-13.

LISTING 15-13: The beginnings of the Initialize() method

VB

Public Overrides Sub Initialize(ByVal name As String, _
    ByVal config As System.Collections.Specialized.NameValueCollection)
    MyBase.Initialize(name, config)
End Sub

C#

public override void Initialize(string name,
    System.Collections.Specialized.NameValueCollection config)
{
    base.Initialize(name, config);
}

The Initialize() method takes two parameters. The first parameter is the name of the parameter. The second is the name/value collection from the provider declaration in the web.config file. This collection includes all the attributes and their values, such as the xmlUserDatabaseFile attribute and the value of the name of the XML file that holds the user information. Using config, you can gain access to these defined values.

For the XmlFileProvider instance, you address the applicationName attribute and the xmlUserDatabaseFile attribute as shown in Listing 15-14.

LISTING 15-14: Extending the Initialize() method

VB

Public Overrides Sub Initialize(ByVal name As String, _
    ByVal config As System.Collections.Specialized.NameValueCollection)
    MyBase.Initialize(name, config)
 
    _AppName = config("applicationName")
 
    If (String.IsNullOrEmpty(_AppName)) Then
        _AppName = "/"
    End If
 
    _FileName = config("xmlUserDatabaseFile")
 
    If (String.IsNullOrEmpty(_FileName)) Then
        _FileName = "/App_Data/Users.xml"
    End If
End Sub

C#

public override void Initialize(string name,
    System.Collections.Specialized.NameValueCollection config)
{
    base.Initialize(name, config);
 
    _AppName = config["applicationName"];
 
    if (String.IsNullOrEmpty(_AppName))
    {
        _AppName = "/";
    }
 
    _FileName = config["xmlUserDatabaseFile"];
 
    if (String.IsNullOrEmpty(_FileName))
    {
        _FileName = "/App_Data/Users.xml";
    }
}

Besides performing the initialization using MyBase.Initialize(), you retrieve both the applicationName and xmlUserDatabaseFile attributes’ values using config. In all cases, you should first check whether the value is either null or empty. You use the String.IsNullOrEmpty() method to assign default values if the attribute is missing for the provider declaration in the web.config file. In the case of the XmlFileProvider instance, this is, in fact, the case. The applicationName attribute in the XmlFileProvider declaration is actually not declared and, for this reason, the default value of / is assigned as the value.

In the case of the xmlUserDatabaseFile attribute, a value is provided. If no value is provided in the web.config file, the provider looks for an XML file named Users.xml found in the App_Data folder.

Validating Users

One of the more important features of the membership provider is that it validates users (it authenticates them). The validation of users is accomplished through the ASP.NET Login server control. This control, in turn, makes use of the Membership.ValidateUser() method that ends up using the ValidateUser() method in the XmlMembershipProvider class.

Now that the Initialize() method and private variables are in place, you can start giving the provider some functionality. Listing 15-15 presents the implementation of the ValidateUser() method.

LISTING 15-15: Implementing the ValidateUser() method

VB

Public Overrides Function ValidateUser(username As String, password As String) _ 
    As Boolean
    If (String.IsNullOrEmpty(username) Or String.IsNullOrEmpty(password)) Then
        Return False
    End If
 
    Try
        ReadUserFile()
 
        Dim mu As MembershipUser
 
        If (_MyUsers.TryGetValue(username.ToLower(), mu)) Then
            If (mu.Comment = password) Then
                Return True
            End If
        End If
 
        Return False
    Catch ex As Exception
        Throw New Exception(ex.Message.ToString())
    End Try
End Function

C#

public override bool ValidateUser(string username, string password)
{
    if (String.IsNullOrEmpty(username) || String.IsNullOrEmpty(password))
    {
        return false;
    }
 
    try
    {
        ReadUserFile();
 
        MembershipUser mu;
 
        if (_MyUsers.TryGetValue(username.ToLower(), out mu))
        {
            if (mu.Comment == password)
            {
                return true;
            }
        }
 
        return false;
    }
    catch (Exception ex)
    {
        throw new Exception(ex.Message.ToString());
    }
}

Looking over the ValidateUser() method, you can see that it takes two parameters: the username and the password of the user (both of type String). The value returned from ValidateUser() is a Boolean — just a True or False value to inform of the success or failure of the validation process.

One of the first operations performed in the ValidateUser() method is a check to determine whether either the username or the password is missing from the invocation. If one of these items is missing in the request, a False value is returned.

From there, a Try. . .Catch is done to check whether the user and the user’s password are included in the XML file. The process of getting the user information out of the XML file and into the MyUsers variable is done by the ReadUserFile() method. This method is described shortly, but the important concept is that the _MyUsers variable is an instance of the Dictionary generic class. The key is a lowercase string value of the username, whereas the value is of type MembershipUser, a type provided via the membership system.

After the _MyUsers object is populated with all users in the XML file, a MembershipUser instance is created. This object is the output of a TryGetValue operation. The MembershipUser does not contain the password of the user, and for this reason, the ReadUserFile() method makes the user’s password the value of the Comment property of the MembershipUser class. If the username is found in the dictionary collection, the password of that particular MembershipUser instance is compared to the value in the Comment property. The return value from the ValidateUser() method is True if they are found to be the same.

As you can see, this method really is dependent upon the results that come from the ReadUserFile() method, which is covered next.

Building the ReadUserFile() Method

The ReadUserFile() method reads the contents of the XML file that contains all the users for the application. This method is a custom method, and its work is done outside of the ValidateUser() method. This means you can reuse it in other methods that you might want to implement (such as the GetAllUsers() method). The only job of the ReadUserFile() method is to read the contents of the XML file and place all the users in the _MyUsers variable, as shown in Listing 15-16.

LISTING 15-16: The ReadUserFile() method to get all the users of the application

VB

Private Sub ReadUserFile()
    If (_MyUsers Is Nothing) Then
        SyncLock (Me)
            _MyUsers = New Dictionary(Of String, MembershipUser)()
            Dim query = From users In _
                        XElement.Load( _
                            HostingEnvironment.MapPath(_FileName)).Elements("User") _
                        Select users
 
            For Each user In query
                Dim mu As MembershipUser = New MembershipUser(Name, _
                   user.Element("Username").Value, _
                   Nothing, _
                   user.Element("Email").Value, _
                   String.Empty, _
                   user.Element("Password").Value, _
                   True, _
                   False, _
                   DateTime.Parse(user.Element("DateCreated").Value), _
                   DateTime.Now, _
                   DateTime.Now, _
                   DateTime.Now, _
                   DateTime.Now)
 
                _MyUsers.Add(mu.UserName.ToLower(), mu)
            Next
        End SyncLock
    End If
End Sub

C#

private void ReadUserFile()
{
    if (_MyUsers == null)
    {
        lock (this)
        {
            _MyUsers = new Dictionary<string, MembershipUser>();
            var query = from users in 
                        XElement.Load(
                          HostingEnvironment.MapPath(_FileName)).Elements("User")
                        select users;
 
            foreach (var user in query)
            {
                MembershipUser mu = new MembershipUser(Name,
                    user.Element("Username").Value,
                    null,
                    user.Element("Email").Value,
                    String.Empty,
                    user.Element("Password").Value,
                    true,
                    false,
                    DateTime.Parse(user.Element("DateCreated").Value),
                    DateTime.Now,
                    DateTime.Now,
                    DateTime.Now,
                    DateTime.Now);
 
                _MyUsers.Add(mu.UserName.ToLower(), mu);
            }
        }
    }
}

NOTE You need to import the System.Xml, System.Xml.Linq, and System.Web.Hosting namespaces for this code to work.

The first action of the ReadUserFile() method is to place a lock on the action that is going to occur in the thread being run. This is a unique feature in ASP.NET. When you are writing your own providers, be sure you use thread-safe code. For most items that you write in ASP.NET, such as an HttpModule or an HttpHandler (covered in Chapter 30), you don’t need to make them thread-safe. These items may have multiple requests running on multiple threads, and each thread making a request to either the HttpModule or the HttpHandler sees a unique instance of these items.

Unlike an HttpHandler, only one instance of a provider is created and utilized by your ASP.NET application. If multiple requests are being made to your application, all these threads are trying to gain access to the single provider instance contained in the application. Because more than one request might be coming into the provider instance at the same time, you should create the provider in a thread-safe manner. You can do so by using a lock operation when performing tasks such as file I/O operations. To lock the access, use the SyncLock (for Visual Basic) and the lock (for C#) statements in the ReadUserFile() method.

The advantage of this code construction, however, is that a single instance of the provider is running in your application. After the _MyUsers object is populated with the contents of the XML file, you have no need to repopulate the object. The provider instance doesn’t just disappear after a response is issued to the requestor. Instead, the provider instance is contained in memory and utilized for multiple requests, which is the reason for checking whether _MyUsers contains any values before reading the XML file.

If you find that _MyUsers is null, use LINQ to XML to get at every <User> element in the document. For each <User> element in the document, you assign the values to a MembershipUser instance. The MembershipUser object takes the following arguments:

public MembershipUser (
    string providerName,
    string name,
    Object providerUserKey,
    string email,
    string passwordQuestion,
    string comment,
    bool isApproved,
    bool isLockedOut,
    DateTime creationDate,
    DateTime lastLoginDate,
    DateTime lastActivityDate,
    DateTime lastPasswordChangedDate,
    DateTime lastLockoutDate
)

Although you do not provide a value for each and every item in this construction, the values that are really needed are pulled from the XML file using the XElement object. Then, after the MembershipUser object is populated with everything you want, the next job is to add these items to the _MyUsers object using the following:

_MyUsers.Add(mu.UserName.ToLower(), mu);

With the ReadUserFile() method in place, as stated, you can now use it in more than the ValidateUser() method. Remember that after the _MyUsers collection is populated, you don’t need to repopulate the collection again. Instead, it remains in place for the other methods to use. Next, this chapter looks at using what has been demonstrated so far in your ASP.NET application.

Using the XmlMembershipProvider for User Login

If you have made it this far in the example, you do not need to do much more to make use of the XmlMembershipProvider class. At this point, you should have the XML data file in place that is a representation of all the users of your application (this XML file appears earlier in Listing 15-7) and the XmlFileProvider declaration in the web.config file of your application (the changes to the web.config file appear in Listing 15-8). Of course, another necessary item is either the XmlMembershipProvider.vb or .cs class in the App_Code folder of your application. However, if you built the provider using the Class Library project template, you want to just make sure the DLL created is referenced correctly in your ASP.NET application (which means the DLL is in the Bin folder). After you have these items in place, getting started with using the provider is simple.

For a quick example, simply create a Default.aspx page that has only the text You are authenticated!

Next, you create a Login.aspx page, and place a single Login server control on the page. You won’t need to make any other changes to the Login.aspx page besides placing the control. Users can now log in to the application.


NOTE For information on the membership system, which includes detailed explanations of the various server controls it offers, visit Chapter 19.

When you have those two files in place within your mini-ASP.NET application, the next step is to make some minor changes to the web.config file to allow for Forms authentication and to deny all anonymous users to view any of the pages. Listing 15-17 presents this bit of code.

LISTING 15-17: Denying anonymous users to view the application in the web.config file

<?xml version="1.0"?>
<configuration>
    <system.web>
        <compilation debug="true" targetFramework="4.5" />
        <httpRuntime targetFramework="4.5" />
        <authentication mode="Forms"/>
        <authorization>
            <deny users="?"/>
        </authorization>
        <!-- Other settings removed for clarity -->
    </system.web>
</configuration>

Now, run the Default.aspx page, and you are immediately directed to the Login.aspx page (you should have this file created in your application and it should contain only a single Login server control) where you apply one of the username and password combinations that are present in the XML file. It is as simple as that!

The nice thing with the provider-based model found in ASP.NET 4.5 is that the controls that are working with the providers don’t know the difference when these large changes to the underlying provider are made. In this example, you have removed the default SqlMembershipProvider and replaced it with a read-only XML provider. When the end user clicks the Log In button within the Login server control, the control is still simply making use of the Membership.ValidateUser() method, which is working with the XmlMembershipProvider that was just created. As you should see by now, this model is powerful.

EXTENDING PREEXISTING PROVIDERS

In addition to building your own providers from one of the base abstract classes such as MembershipProvider, another option is to simply extend one of the preexisting providers that come with ASP.NET.

For example, you might be interested in using the membership and role management systems with SQL Server but want to change how the default providers (SqlMembershipProvider or SqlRoleProvider) work under the covers. If you are going to work with an underlying data store that is already utilized by one of the providers available out of the box, actually changing the behavior of the available provider makes a lot more sense than building a brand-new provider from the ground up.

The other advantage of working from a preexisting provider is that no need exists to override everything the provider exposes. Instead, if you are interested in changing only a particular behavior of a built-in provider, you might only need to override a couple of the exposed methods and nothing more, making this approach rather simple and quick to achieve in your application.

Next, this chapter looks at extending one of the built-in providers to change the underlying functionality of the provider.

Limiting Role Capabilities with a New LimitedSqlRoleProvider Provider

Suppose you want to utilize the role management system in your ASP.NET application and have every intention of using a SQL Server backend for the system. Suppose you also want to limit what roles developers can create in their applications, and you want to remove their capability to add users to a particular role in the system.

Instead of building a role provider from scratch from the RoleProvider abstract class, deriving your provider from SqlRoleProvider and simply changing the behavior of a few methods that deal with the creation of roles and adding users to roles makes more sense.

For this example, create the provider in your application within the App_Code folder as before. In reality, however, you probably want to create a Class Library project if you want to use this provider across your company so that your development teams can use a DLL rather than a modifiable class file.

Within the App_Code folder, create a class file called LimitedSqlRoleProvider.vb or.cs. You want this class to inherit from SqlRoleProvider, and this gives you the structure shown in Listing 15-18.

LISTING 15-18: The beginnings of the LimitedSqlRoleProvider class

VB

Imports System.Web.Security
 
Public Class LimitedSqlRoleProvider
    Inherits SqlRoleProvider
 
End Class

C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
 
namespace Chapter15_CustomProviders_CS.App_Code
{
    public class LimitedSqlRoleProvider : SqlRoleProvider
    {
    }
}

Creating this class is similar to creating the XmlMembershipProvider class. When you did that, however, you were able to use Visual Studio to build the entire class skeleton of all the methods and properties you had to override to get the new class up and running. In this case, if you try to do the same thing in Visual Studio, you get an error (if using C#) or, perhaps, no result at all (if using Visual Basic) because you are not working with an abstract class. You do not need to override an enormous number of methods and properties. Instead, because you are deriving from a class that already inherits from one of these abstract classes, you can get by with overriding only the methods and properties that you must work with and nothing more.

To get at this list of methods and properties within Visual Studio, you simply type Public Overrides (when using Visual Basic) or public override (when using C#). IntelliSense then provides you with a large drop-down list of available methods and properties to work with, as shown in Figure 15-6.

FIGURE 15-6

image

For this example, you only override the CreateRole(), AddUsersToRoles(), and DeleteRole() methods. They are described next.

The CreateRole() Method

The CreateRole() method in the SqlRoleProvider class enables developers to add any role to the system. The only parameter required for this method is a string value that is the name of the role. For this example, instead of letting developers create any role they want, this provider limits the role creation to only the Administrator and Manager roles. To accomplish this in the CreateRole() method, you code the method as presented in Listing 15-19.

LISTING 15-19: Allowing only the Administrator or Manager role in the CreateRole() method

VB

Public Overrides Sub CreateRole(ByVal roleName As String)
    If (roleName = "Administrator" Or roleName = "Manager") Then
        MyBase.CreateRole(roleName)
    Else
        Throw New  _
            ProviderException("Role creation limited to only Administrator and Manager")
    End If
End Sub

C#

public override void CreateRole(string roleName)
{
    if (roleName == "Administrator" || roleName == "Manager")
    {
        base.CreateRole(roleName);
    }
    else
    {
        throw new
            ProviderException("Role creation limited to only Administrator and Manager");
    }
}

NOTE You need to import the System.Configuration.Provider namespace for this code to work.

In this method, you can see that a check is first done to determine whether the role being created is either Administrator or Manager. If the role being created is not one of these defined roles, a ProviderException is thrown informing the developer of which roles he or she is allowed to create.

If Administrator or Manager is one of the roles, the base class (SqlRoleProvider. CreateRole() method is invoked.

The DeleteRole() Method

If you allow developers using this provider to create only specific roles, you might not want them to delete any role after it is created. If this is the case, you want to override the DeleteRole() method of the SqlRoleProvider class, as shown in Listing 15-20.

LISTING 15-20: Disallowing the DeleteRole() method

VB

Public Overrides Function DeleteRole(ByVal roleName As String, _
    ByVal throwOnPopulatedRole As Boolean) As Boolean
    Return False
End Function

C#

public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
{
    return false;
}

Looking at the DeleteRole() method, you can see that deleting any role is completely disallowed. A false value is returned and no action is taken instead of raising the base class’s DeleteRole() and returning the following:

return this.DeleteRole(roleName, throwOnPopulatedRole);

Another approach is to throw a NotSupportedException, as shown here:

throw new NotSupportedException();

The AddUsersToRoles() Method

As you look over the methods that can be overridden, notice that only one single method enables you to add any number of users to any number of roles. Multiple methods in the Roles class actually map to this method. If you look at the Roles class, notice the AddUserToRole(), AddUserToRoles(), AddUsersToRole(), and AddUsersToRoles() methods at your disposal. All these actually map to the AddUsersToRoles() method that is available in the RoleProvider base class.

For example, suppose you want to enable developers to add users only to the Manager role but not to add any users to the Administrator role. You could accomplish something like this by constructing a method, as shown in Listing 15-21.

LISTING 15-21: Disallowing users to be added to a particular role

VB

Public Overrides Sub AddUsersToRoles(ByVal usernames() As String, _
    ByVal roleNames() As String)
 
    For Each roleItem As String In roleNames
        If roleItem = "Administrator" Then
            Throw New  _
                ProviderException("You are not authorized to add any users" & _
                    " to the Administrator role")
        End If
    Next
 
    MyBase.AddUsersToRoles(usernames, roleNames)
End Sub

C#

public override void AddUsersToRoles(string[] usernames, string[] roleNames)
{
    foreach (string roleItem in roleNames)
    {
        if (roleItem == "Administrator")
        {
            throw new ProviderException("You are not authorized to add any users" +
                " to the Administrator role");
        }
    }
 
    base.AddUsersToRoles(usernames, roleNames);
}

This overridden method iterates through all the provided roles, and if one of the roles contained in the string array is the role Administrator, a ProviderException instance is thrown informing the developer that he or she is not allowed to add any users to this particular role. Although it is not shown here, you can also take the same approach with the RemoveUsersFromRoles() method exposed from the RoleProvider base class.

Using the New LimitedSqlRoleProvider Provider

After you have the provider in place and ready to use, you have to make some modifications to the web.config file in order to use this provider in your ASP.NET application. You learn how you add what you need to the web.config file for this provider in Listing 15-22.

LISTING 15-22: Making the appropriate changes to the web.config file for the provider

<?xml version="1.0"?>
<configuration>
    <system.web>
        <roleManager defaultProvider="LimitedProvider" enabled="true">
            <providers>
                <add connectionStringName="LocalSqlServer" applicationName="/"
                 name="LimitedProvider"
                 type="LimitedSqlRoleProvider" />
            </providers>
        </roleManager>
    </system.web>
</configuration>

Remember that you have to define the provider to use in your application by providing a value for the defaultProvider attribute and defining that provider further in the <providers> section. You also have to enable the provider by setting the enabled attribute to true. By default, the role management system is disabled.

Using the <add> element, you can add a provider instance that makes use of the LimitedSqlRoleProvider class. Because this provider derives from the SqlRoleProvider class, you must use some of the same attributes that this provider requires, such as the connectionStringName attribute that points to the connection string to use to connect to the specified SQL instance.

After you have the new LimitedSqlRoleProvider instance in place and defined in the web.config file, you can use the Roles class in your application just as you normally would, but notice the behavior of this class is rather different from the normal SqlRoleProvider.

To see it in action, construct a simple ASP.NET page that includes a TextBox, Button, and Label server control. The page should appear as shown in Listing 15-23.

LISTING 15-23: Using Roles.CreateRole()

VB

<%@ Page Language="VB" %>
<script runat="server">
    Protected Sub Button1_Click(ByVal sender As Object, _
      ByVal e As System.EventArgs)
        
        Try
            Roles.CreateRole(TextBox1.Text)
            Label1.Text = "Role successfully created."
        Catch ex As Exception
            Label1.Text = ex.Message.ToString()
        End Try
    End Sub
</script>
<!DOCTYPE html>
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>>Main Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        Role Name:<br />
        <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox><br />
        <br />
        <asp:Button ID="Button1" runat="server" Text="Create Role"
            OnClick="Button1_Click" /><br />
        <br />
        <asp:Label ID="Label1" runat="server"></asp:Label>
    </div>
    </form>
</body>
</html>

C#

<%@ Page Language="C#" %>
<script runat="server">
    protected void Button1_Click(object sender, EventArgs e)
    {
        try
        {
            Roles.CreateRole(TextBox1.Text);
            Label1.Text = "Role successfully created.";
        }
        catch (Exception ex)
        {
            Label1.Text = ex.Message.ToString();
        }
    }
</script>

This simple ASP.NET page enables you to type in a string value in the textbox and to attempt to create a new role using this value. Note that anything other than the role Administrator and Manager results in an error. So, when the Roles.CreateRole() is called, an error is produced if the rules defined by the provider are not followed. In fact, running this page and typing in a role other than the Administrator or Manager role gives you the results presented in Figure 15-7.

FIGURE 15-7

image

To show this provider in action, create another ASP.NET page that allows you to add users to a particular role. As stated, you can do so with a number of available methods, but in this case, this example uses the Roles.AddUserToRole() method, shown in Listing 15-24.

LISTING 15-24: Attempting to add users to a role through the new role provider

VB

<%@ Page Language="vb" %>
<script runat="server">
    Protected Sub Button1_Click(ByVal sender As Object, _
      ByVal e As System.EventArgs)
        
        Try
            Roles.AddUserToRole(TextBox1.Text, TextBox2.Text)
            Label1.Text = "User successfully added to role"
        Catch ex As Exception
            Label1.Text = ex.Message.ToString()
        End Try
    End Sub
</script>
<!DOCTYPE html>
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Main Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        Add the following user:<br />
        <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox><br />
        <br />
        To role:<br />
        <asp:TextBox ID="TextBox2" runat="server"></asp:TextBox><br />
        <br />
        <asp:Button ID="Button1" runat="server" Text="Add User to Role"
            OnClick="Button1_Click" /><br />
        <br />
        <asp:Label ID="Label1" runat="server"></asp:Label>
    </div>
    </form>
</body>
</html>

C#

<%@ Page Language="C#" %>
<script runat="server">
    protected void Button1_Click(object sender, EventArgs e)
    {
        try
        {
            Roles.AddUserToRole(TextBox1.Text, TextBox2.Text);
            Label1.Text = "User successfully added to role";
        }
        catch (Exception ex)
        {
            Label1.Text = ex.Message.ToString();
        }
    }
</script>

In this example, two textboxes are provided. The first asks for the username and the second asks for the role to add the user to. The code for the button click event uses the Roles.AddUserToRole() method. Because you built the provider, you know that an error is thrown if there is an attempt to add a user to the Administrator role. This attempt is illustrated in Figure 15-8.

FIGURE 15-8

image

In this case, an attempt was made to add Jason to the Administrator role. This, of course, throws an error and returns the error message that is defined in the provider.

SUMMARY

In this chapter and the previous chapter, you were provided with an overview of the provider model and what it means to the ASP.NET 4.5 applications you build today. Although a lot of providers are available to you out of the box to use for interacting with one of the many systems provided in ASP.NET, you are not limited to just these providers. You definitely can either build your own providers or extend the functionality of the providers already present in the system.

This chapter looked at both of these scenarios. First, you built your own provider to use the membership system with an XML data store for the user data, and then you worked through an example of extending the SqlRoleProvider class (something already present in ASP.NET) to change the underlying behavior of this provider.