ASP.NET: Integrating Customized Roles, Membership and Profiles in ASP.NET 4.0

Learn to configure and use the ASP.NET Membership providers to support customized roles and membership profiles in ASP.NET 4.0. You can use ASPNET_REGSQL or utility methods under System.Web.Management.

My goal in this short piece is to provide an example of how to pull them all together. We'll use the standard Membership Provider for authentication, the standard Role provider for Authorization, and we'll add a completely  custom Profile example to show how custom user Profile data can be stored on a per-user basis, even for anonymous users, and then automatically migrated to their full User profile when they actually "sign up" on your site.

You can set this up with any SQL Server database you want, including either a new or existing one. Plus, I'll include a SQL Script that will show you how to install Membership, Role and Profile required database information on a hosted site, and even an ASP.NET "SetupASPNETDatabase.aspx" page that does all this programmatically - something many developers aren't even aware exists. Finally, I'll include some nice "Admin" pages, courtesy of my good friend and fellow MVP Peter Kellner, to allow Administrators to work with users and roles online, and point you to some additional resources that have become available.

In short, you'll have a basic example code framework and controls to do just about anything you would expect to need to perform in ASP.NET for a site, using all three providers - Membership, Role, and Profile. So, let's get started.

The first thing we want to do before we even open the sample solution is to enable our database for the providers. There are actually three separate ways you can do this:

1) Run ASPNET_REGSQL and follow the prompts. Of course, this requires that you have command-prompt access to the machine on which your site will be deployed. On my x64 machine, this can be found in C:\Windows\Microsoft.NET\Framework64\v4.0.30319

2) Run ASP.NET_REGSQL on your own sample database locally, and EXPORT all the sql script necessary to create the tables, views and stored procedures on a remote database. Again, this assumes you have the ability to access a remote hosted site's SQL Server online and execute a rather large SQL Script on it. A sample script is included in the "SQL" Folder in the download. If you can run this against your remote hosted SQL Server Database, that will be fine. It's important to understand that the ASPNETDB.MDF default database that is provided with an ASP.NET Web Application is not necessary - you really should and would want to install all the tables and stored procs in your own site database, and not use this default one at all.   You can export complete script for a database with SQL Publishing Wizard. Mine is at C:\Program Files (x86)\Microsoft SQL Server\90\Tools\Publishing\1.4\SqlPubWiz.exe . If you do not have this, you can download and install it.

3) Do it programmatically: Make a "Setup.aspx" page that uses the System.Web.Management utility method:

Management.SqlServices.Install("server", "USERNAME", "PASSWORD", "databasename", SqlFeatures.All)

This does everything that ASPNET_REGSQL does. You'd think they would make it more obvious that you can do this, given the large number of sites that are hosted by commercial shared hosting companies, but they decided to push ASPNET_REGSQL as if everybody everywhere automatically has access to it. There is a page "SetUpASPNetDatabase.aspx" in the sample solution where you can simply fill in the above parameters in a form, and press a button to set up your database with ASP.NET Membership, Roles and Profile. You would only need to run this page "one time".

So, now we have our database (either SQL Server 2005 / SQLEXPRESS or SQL Server 2008 / SQLExpress) set up for Membership, Roles and Profiles. The rest is just figuring out what code and what controls to use to make everything work. Fortunately, ASP.NET 2.0 and higher provide a group of controls that handle most of these tasks out of the box and can also be highly customized beyond their default behaviors.

The easiest way to review this process is to go through it in stages with the sample application open in Visual Studio.NET 2010. So, unzip the sample into your favorite folder, and double-click on the "MembershipRolesProfile.sln" solution file to bring it all up. This is configured as a Web Application Project.

The sample database for this exercise is "Articles" so go ahead and create a new database of that name now, and then run the "TablesSprocAndData.sql" Sql script in the "SQL" subfolder against it. This creates the table and stored procs that are used in the sample, so that we don't need to "reinvent the wheel". Next, if you haven't set up your new database for Membership, Roles and Profiles, you can either execute the stock "ASPNETREGSQLFULL.sql" script that I have prepared as in "2" above, or you can "View In Browser" on the SetupASPNETDatabase.aspx page in the solution to see how the option "3" above works. Fill in Server, Username, password and database names, and press the "Set Up Database" button, and the SqlServices.Install Method will be run. Finally, make sure the connection strings in the web.config actually work for your specific environment, otherwise you won't get very far.

At this point, all your infrastructure for the sample is set up, and we are ready to look at some code. If you look at the Default.aspx page, it has a Repeater to display content, and sports a LoginStatus control pointing to "~/Login.aspx" as its login/ logout page action. Now let's move to the Login.aspx page.

You can see that Login.aspx has only a Login Control. The property settings on this control are where we can take advantage of our Membership Provider automatically. In this case, our MembershipProvider is set to "DefaultMembershipProvider", and whatever we have in our web.config for that now takes over. In my codebehind for this control, I have only the following:

protected void Login1_Authenticate(object sender, AuthenticateEventArgs e)
    {
        string userName = Login1.UserName.ToString();
        string password = Login1.Password.ToString();
         if (Membership.ValidateUser(userName, password))
         {
             if (Request.QueryString["ReturnUrl"] != null)
            {          
                 FormsAuthentication.RedirectFromLoginPage(userName,true);
            }
            else
            {
                FormsAuthentication.SetAuthCookie(userName, true);
                 Response.Redirect("~/Default.aspx");
            }
        }
         else
        {
         lblResults.Visible = true;
         lblResults.Text = "Unsuccessful login. Please re-enter your information and try again.";
         if ((Membership.GetUser(userName) != null) &&
               (Membership.GetUser(userName).IsLockedOut == true))
              lblResults.Text += "  Your account has been locked out.";
        }
    }   
  
So, I am using the Login Control to employ the Membership Provider's "ValidateUser" method, with Forms Authentication, which is already set up in the web.config.

Obviously, if you aren't a "member" yet, you'll want to click the "Create Account" link on the Login Control, and this will take you to the "Admin.aspx" page, where we have a CreateUserWizard, a ChangePassword, and a PasswordRecovery control. All the settings on these are highly configurable; in most cases you will not need to write any code to use them. In my CreateUserWizard Control, I am using the following codebehind:

protected void CreateUserWizard1_CreatedUser(object sender, EventArgs e)
{
if(!Roles.RoleExists("user")) Roles.CreateRole("user");
string username = this.CreateUserWizard1.UserName;
Roles.AddUserToRole(username, "user");
}    

If the User Role doesn't exist yet (first time the app is ever used) we create one, and then we Add this user to the "user" role. The rest of the CreateUserWizard process doesn't require any custom code - its automatic, based on the property settings of the control. You can check for and add additional roles here if you like. The only thing we have not done is to make the user an "Admin" role user, and that's why I have commented out the <allow roles... element in the Admin section web.config. Once you have an Admin user (you can do this in the Admin section) you can create an /Admin folder with your admin-only pages and provide a separate web.config in it to only allow Admin users into this section of the site.

Now let's take a look at some Profile Features. Start the application, but do not log in or create an account. Go to the Profile page, and fill in some Profile data. Then, press the Get Profile Values button to see that your Profile data has been saved as an anonymous user. You can close your browser, and come back again, and see that we are indeed holding Profile data for you, even as an anonymous user. Now, create a new account for yourself and log in. Then, go to the Profile page again, and you will see that we have migrated your anonymous data to your new "real member" Profile. This is done in the new "OnMigrateAnonymous" event which fires in Global.asax:

public void Profile_OnMigrateAnonymous(Object sender, ProfileMigrateEventArgs args)
    {
        ProfileCommon anonymousProfile;
        anonymousProfile =Profile.GetProfile(args.AnonymousID);
         if (Profile.LastActivityDate == DateTime.MinValue)
        {
            Profile.UserDetails.Address = anonymousProfile.UserDetails.Address;
            Profile.UserDetails.City = anonymousProfile.UserDetails.City;
          
            Profile.UserDetails.State = anonymousProfile.UserDetails.State;
            Profile.UserDetails.Zip = anonymousProfile.UserDetails.Zip;
            Profile.UserDetails.Email = anonymousProfile.UserDetails.Email;
            Profile.UserDetails.Phone = anonymousProfile.UserDetails.Phone;
            Profile.UserDetails.GetNewsletter = anonymousProfile.UserDetails.GetNewsletter;
             Profile.Save();
        }         
        // delete the anonymous user data, user is no longer anonymous
        ProfileManager.DeleteProfile(anonymousProfile.UserName);
        //delete the anonymous cookie so this event no longer fires for a logged-in user:
        AnonymousIdentificationModule.ClearAnonymousIdentifier();
    }      
  
Now, let's take a look at the web.config setup for all this, and how it works:

<?xml version="1.0"?>
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
  <connectionStrings>
    <remove name="LocalSqlServer"/>
    <add name="LocalSqlServer" connectionString="server=localhost;database=Articles;Integrated Security=SSPI" providerName="System.Data.SqlClient"/>
  </connectionStrings>
  <appSettings>
    <add key="articleFolder" value="Articles"/>
  
  </appSettings>
  <system.web>
    <anonymousIdentification enabled="true"/>
    <!-- profile-->
    <profile>
      <properties>
        <group name="UserDetails">
          <add name="State" type="System.String" allowAnonymous="true"/>
          <add name="Email" type="System.String" allowAnonymous="true"/>
          <add name="Address" type="System.String" allowAnonymous="true"/>
          <add name="City" type="System.String" allowAnonymous="true"/>
          <add name="Zip" type="System.String" allowAnonymous="true"/>
          <add name="Phone" type="System.String" allowAnonymous="true"/>
          <add name="GetNewsletter" type="System.Boolean" allowAnonymous="true"/>
        </group>
      </properties>
    </profile>
    <!-- membership provider -->
     <roleManager enabled="true" cacheRolesInCookie="true" createPersistentCookie="true">
      <providers>
        <add applicationName="/" connectionStringName="LocalSqlServer" name="DefaultRoleProvider" type="System.Web.Security.SqlRoleProvider"/>
      </providers>
    </roleManager>
    <membership defaultProvider="DefaultMembershipProvider">      
      <providers>
        <remove name="ASPNETSqlMembershipProvider" />
        <add connectionStringName="LocalSqlServer" enablePasswordRetrieval="true"  enablePasswordReset="true" requiresQuestionAndAnswer="false" applicationName="/" requiresUniqueEmail="true" passwordFormat="Clear" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0"      passwordStrengthRegularExpression=""  name="DefaultMembershipProvider" type="System.Web.Security.SqlMembershipProvider"/>
       </providers>
    </membership>
    <compilation debug="true" targetFramework="4.0">
  </compilation>
    <authentication mode="Forms">
      <forms name=".SITE1" loginUrl="Login.aspx" protection="All" timeout="30" path="/" requireSSL="false" slidingExpiration="true" defaultUrl="Default.aspx" enableCrossAppRedirects="true"/>
    </authentication>
    <pages controlRenderingCompatibilityVersion="3.5" clientIDMode="AutoID"/>
  </system.web>
  <location path="SamplePages">
    <system.web>
       <authorization>
        <allow roles="Administrator"/>
        <deny users="*"/>
      </authorization>
    </system.web>
  </location>
</configuration>

In the beginning, I start with the ConnectionStrings section, removing the Default LocalSqlServer Provider (which defaults to SQLExpress) so that I can add back in my own, specifying the connection string and database.

Next, I specify that anonymousIdentification is enabled. This is what lets us store Profile Data for the anonymous users, and migrate it to their permanent Profile when they join the site. Then, I have my profile section where I actually set up the profile items I want to store. If you look in the MembershipUtilities project in the solution, you'll see the CustomProfile.cs class I've created and how easy it is to override the ProfileBase class. In particular, notice that I've decorated each item with the [System.Web.Profile.SettingsAllowAnonymous(true)] attribute.

Next, I specify the Membership Provider, and it this case I am using the DefaultProvider. Next, the Role provider. Finally I have my Forms Authentication setup. The last entry is a location path block to allow only Administrators access to the SamplePages folder. Originally I was going to write up a page to manage Members and Roles, but then I found Peter Kellner's nice workup and sample on this, so in the interest of my credo "Don't reinvent the wheel", I've included those pages in the app. You can, of course, comment that section out temporarily to allow you access so you can make yourself an Administrator after you log into your own site.  

By the way, for those Master Mechanics who are interested in getting their hands greasy, Scott Guthrie announced some time ago the download of the sample Provider Toolkit,  but since that time developers have written a whole slew of custom providers. This search on Codeplex.com will help you find most of them.  There are currently FIFTY ONE separate offerings of various providers for Membership, Roles and Profile on Codeplex.com alone - just in case you are wondering if this framework has broad acceptance in the developer community.

Finally, be advised that this is not a complete website -- it is just a sample application that was designed solely to illustrate how to pull together the various providers and make them work together. As such, I wouldn't recommend using the solution as the basis for a real web site, but rather, as a test bed to play around with and get used to how the various parts work.

I hope this effort at bringing together examples of how the Membership, Roles and Profile providers work together, in a single article, has been useful to you. You can download the complete Visual Studio 2010 Solution here.

By Peter Bromberg   Popularity  (12343 Views)