Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]
.pdf
C H A P T E R 2 4 ■ P R O F I L E S |
829 |
create a template column that includes a CheckBox control. When the user clicks another button (such as submit), you can loop over all the items in the GridView and check the state of the check box in each item. If the state is checked, you would then add that item to the shopping cart.
Custom Profiles Providers
The profile model plugs neatly into ASP.NET web pages. However, it isn’t very configurable. You might decide you need to create a custom Profiles provider for a number of reasons:
•You need to store profile information in a data source other than a SQL Server database, such as an Oracle database.
•You need your profile data to be available to other applications. Parsing the information in the PropertyValuesString and PropertyValuesBinary fields is tedious, error-prone, and inflexible. If you need to use this information in other queries or applications, you need to store your profile information in a database table that’s split into distinct fields.
•You need to implement additional logic when storing or retrieving profile data. For example, you could apply validation, caching, logging, encryption, or compression. (In some cases, you can get these features by simply extending the ProfileBase class that wraps profile settings, rather than creating an entirely new ProfileProvider.)
In the following sections, you’ll focus on the second scenario. You’ll see how to build a custom provider that keeps its property values in separate fields and can be adapted to fit any existing database.
The Custom Profiles Provider Classes
To implement a Profiles provider, you need to create a class that derives from the ProfileProvider abstract class from the System.Web.Profile namespace. The ProfileProvider abstract class itself inherits the SettingsProvider abstract class from the System.Configuration namespace, which inherits from the ProviderBase abstract class from the System.Configuration.Provider namespace. As a result, you also need to implement members from the SettingsProvider and ProviderBase classes. Altogether, more than a dozen members must be implemented before you can compile your custom Profiles provider.
However, these methods aren’t all of equal importance. For example, you can create a basic provider that saves and retrieves profile information by implementing two or three of these methods. Many of the other methods support functionality that’s exposed by the ProfileManager class, such as the ability to delete profiles or find inactive profiles.
In the following example, you’ll consider a simple Profiles provider that includes the core logic that’s needed to plug into a page but doesn’t support most other parts of the Profiles API. Methods that aren’t supported simply throw a NotImplementedException, like this:
public override int DeleteProfiles(string[] usernames)
{
throw new Exception("The method or operation is not implemented.");
}
All of these methods are conceptually easy to implement (all you need is some basic ADO.NET code). However, properly coding each method requires a fairly substantial amount of code.
Table 24-6 lists all the overridable properties and methods and indicates which class defines them. Those that are implemented in the following example are marked with an asterisk. To be considered truly complete, a provider must implement all of these members.
830 C H A P T E R 2 4 ■ P R O F I L E S
Table 24-6. Abstract Methods for Profiles Providers
Class |
Member |
Description |
*ProviderBase |
Name |
A read-only property that returns the name |
|
|
(set in the web.config file) for the current |
|
|
provider. |
*ProviderBase |
Initialize() |
Gets the configuration element from the |
|
|
web.config file that initializes this provider. |
|
|
Gives you the chance to read custom |
|
|
settings and store the information in |
|
|
member variables. |
SettingsProvider |
ApplicationName |
A name (set in the web.config file) that |
|
|
allows you to separate the users of different |
|
|
applications that are stored in the same |
|
|
database. |
*SettingsProvider |
GetPropertyValues() |
Retrieves the profile information for |
|
|
a single user. This method is called |
|
|
automatically when a web page accesses |
|
|
the Page.Profile property. This method |
|
|
is provided with a list of all the profile |
|
|
properties that are defined in the |
|
|
application. You must return a value |
|
|
for each of these properties. |
*SettingsProvider |
SetPropertyValues() |
Updates the profile information for a single |
|
|
user. This method is called automatically |
|
|
at the end of a request when profile |
|
|
information is changed. This method |
|
|
is provided with a list of all the profile |
|
|
properties that are defined in the |
|
|
application and their current values. |
ProfileProvider |
DeleteProfiles() |
Deletes one or more user profile records |
|
|
from the database. This method has two |
|
|
overloads, which take different parameters. |
ProfileProvider |
DeleteInactiveProfiles() |
Similar to DeleteProfiles() but looks for |
|
|
profiles that haven’t been accessed since |
|
|
a specific time. To support this method, |
|
|
you must keep track of when profiles are |
|
|
accessed or updated in your database. |
ProfileProvider |
GetAllProfiles() |
Returns information about a group of |
|
|
profile records. This method must support |
|
|
paging so that it returns only a subset of |
|
|
the total records. Refer to the aspnet_ |
|
|
Profile_GetProfiles stored procedure that |
|
|
aspnet_regsql creates for a sample paging |
|
|
implementation. |
ProfileProvider |
GetAllInactiveProfiles() |
Similar to GetAllProfiles() but looks for |
|
|
profiles that haven’t been accessed since |
|
|
a specific time. To support this method, |
|
|
you must keep track of when profiles are |
|
|
accessed or updated in your database. |
ProfileProvider |
FindProfilesByUserName() |
Retrieves profile information based on the |
|
|
user name of one or more (if you support |
|
|
wildcard matching) users. The actual |
|
|
profile information isn’t returned—only |
|
|
some standard information such as the last |
|
|
activity date is returned. |
C H A P T E R 2 4 ■ P R O F I L E S |
831 |
Class |
Member |
Description |
ProfileProvider |
FindInactiveProfilesByUser- |
Similar to FindProfilesByUserName () |
|
Name() |
but looks for profiles that haven’t been |
|
|
accessed since a specific time. |
ProfileProvider |
GetNumberOfInactive- |
Counts the number of profiles that haven’t |
|
Profiles() |
been accessed since a specific time. |
|
|
|
* Implemented in the following example
Designing the FactoredProfileProvider
The FactoredProfileProvider stores property values in a series of fields in a database table, rather than in a single block. This makes the values easier to use in different applications and with different queries. Essentially, the FactoredProfileProvider unlocks the profiles table so that it’s no longer using a proprietary schema. The only disadvantage to this approach is that it’s no longer possible to change the profile or add information to it without modifying the schema of your database.
When implementing a custom Profiles provider, you need to determine how generic you want your solution to be. For example, if you decide to implement compression using the classes in the System.IO.Compression namespace (see Chapter 13) or encryption with the classes in the System.Security.Cryptography namespace (see Chapter 25), you’ll also need to decide whether you want to create an all-purpose solution or a more limited provider that’s fine-tuned for your specific scenario.
Similarly, the FactoredProfileProvider has two possible designs:
•You can create a provider that’s designed specifically for your database schema.
•You can create a generic provider that can work with any database table by making certain assumptions. For example, you can simply assume that profile properties match field names.
The first approach is the most straightforward and in some cases will be the easiest to secure and optimize. However, it also limits your ability to reuse your provider or change your database schema later. The second approach is the one you’ll see in the following example.
The basic idea behind the FactoredProfileProvider is that it will perform its two key tasks (retrieving and updating profile information) through two stored procedures. That gives you a powerful layer of flexibility, because you can modify the stored procedures at any time to use different tables, field names, data types, and even serialization choices.
The critical detail in this example is that the web application chooses which stored procedures to use by using the provider declaration in the web.config file. Here’s an example of how you might use the FactoredProfileProvider in an application:
<profile defaultProvider="FactoredProfileProvider"> <providers >
<clear />
<add name="FactoredProfileProvider" type="FactoredProfileProvider" connectionStringName="SqlServices" updateUserProcedure="Users_Update" getUserProcedure="Users_GetByUserName"/>
</providers> <properties>...</properties>
</profile>
Along with the expected attributes (name, type, and connectionStringName), the <add> tag includes two new attributes: updateUserProcedure and getUserProcedure. The updateUserProcedure
832C H A P T E R 2 4 ■ P R O F I L E S
indicates the name of the stored procedure that’s used to insert and update profile information. The getUserProcedure indicates the name of the stored procedure that’s used to retrieve profile information.
This design allows you to use the FactoredProfileProvider with any database table. But what about mapping the properties to the appropriate columns? You could take a variety of approaches to make this possible, but the FactoredProfileProvider takes a convenient shortcut. When updating, it simply assumes that every profile property you define corresponds to the name of a stored procedure parameter. So, if you define the following properties:
<properties>
<add name="FirstName"/> <add name="LastName"/>
</properties>
the FactoredProfileProvider will call the update stored procedure you’ve specified and pass the value in for parameters named @FirstName and @LastName. When querying profile information, the FactoredProfileProvider will look for the field names FirstName and LastName.
This is similar to the design used by the SqlDataSource and ObjectDataSource controls. Although it forces you to follow certain conventions in your two stored procedures, it imposes no other restrictions on the rest of your database. For example, the update stored procedure can insert the information into any series of fields in any table, and the stored procedure used to query profile information can use aliases or joins to construct the expected table.
Coding the FactoredProfileProvider
The first step of creating the FactoredProfileProvider is to derive the class from ProfileProvider:
public class FactoredProfileProvider : ProfileProvider { ... }
All the methods that aren't implemented in this example (see Table 24-6) are simply filled with a single line of code that throws an exception.
■Tip One quick way to fill all the methods with exception-throwing logic is to right-click ProfileProvider in the class declaration and choose Refactor Implement Abstract Class.
Initialization
The FactoredProfileProvider needs to keep track of a few basic details, such as the provider name, the connection string, and the two stored procedures. These details are all exposed through readonly properties, as shown here:
private string name; public override string Name
{
get { return name; }
}
private string connectionString; public string ConnectionString
{
get { return connectionString; }
}
C H A P T E R 2 4 ■ P R O F I L E S |
833 |
private string updateProcedure; public string UpdateUserProcedure
{
get { return updateProcedure; }
}
private string getProcedure; public string GetUserProcedure
{
get { return getProcedure; }
}
To set these details, you need to override the Initialize() method. At this point, you receive a collection that contains all the attributes of the <add> element that registered the provider. If any of the necessary details are absent, you should raise an exception.
public override void Initialize(string name, NameValueCollection config)
{
this.name = name;
// Initialize values from web.config. ConnectionStringSettings connectionStringSettings =
ConfigurationManager.ConnectionStrings[config["connectionStringName"]]; if (connectionStringSettings == null ||
connectionStringSettings.ConnectionString.Trim() == "")
{
throw new HttpException("You must supply a connection string.");
}
else
{
connectionString = connectionStringSettings.ConnectionString;
}
updateProcedure = config["updateUserProcedure"]; if (updateProcedure.Trim() == "")
{
throw new HttpException(
"You must specify a stored procedure to use for updates.");
}
getProcedure = config["getUserProcedure"]; if (getProcedure.Trim() == "")
{
throw new HttpException(
"You must specify a stored procedure to use for retrieving user records.");
}
}
Reading Profile Information
When the web page accesses any profile information, ASP.NET calls the GetPropertyValues() method. It passes in two parameters—a SettingsContext object that includes information such as the current user name and a SettingsPropertyCollection object that contains a collection of all the profile properties that the application has defined (and expects to be able to access). You need to return a SettingsPropertyValueCollection with the corresponding values.
834 C H A P T E R 2 4 ■ P R O F I L E S
Before doing anything, you should create a new SettingsPropertyValueCollection:
public override SettingsPropertyValueCollection GetPropertyValues( SettingsContext context, SettingsPropertyCollection properties)
{
// This collection will store the retrieved values. SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();
...
Now create the ADO.NET objects that you need in order to execute the stored procedure that retrieves the profile information. The connection string and stored procedure name are specified through the configuration attributes that were retrieved in the Initialize() method.
...
SqlConnection con = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand(getProcedure, con);
cmd.CommandType = CommandType.StoredProcedure;
...
The only nonconfigurable assumption in this code is that the stored procedure accepts a parameter named @UserName. You could add other configuration attributes to make this parameter name configurable.
...
cmd.Parameters.Add(new SqlParameter("@UserName", (string)context["UserName"]));
...
Now you’re ready to execute the command and retrieve the matching record. Depending on the design of the database, this record may actually represent the joining of two tables (one with a list of users and one with profile information), or all the information may come from a single table.
...
try
{
con.Open();
SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow);
// Get the first row. reader.Read();
...
Once you have the row, the next task is to loop through the SettingsPropertyCollection. For each defined property, you should retrieve the value from the corresponding field. However, it’s perfectly valid for a user to exist without any profile information. In this case (when reader.HasRows is false), you should still create the SettingsPropertyValue objects for each requested property, but don’t bother setting the property values. They’ll simply keep their defaults.
...
foreach (SettingsProperty property in properties)
{
SettingsPropertyValue value = new SettingsPropertyValue(property);
if (reader.HasRows)
{
value.PropertyValue = reader[property.Name];
}
values.Add(value);
}
...
C H A P T E R 2 4 ■ P R O F I L E S |
835 |
The final step is to close the reader and connection and to return the collection of values.
...
reader.Close();
}
finally
{
con.Close();
}
return values;
}
■Note If you want to mimic the behavior of the SqlProfileProvider, you should also update the database with the last activity time whenever the GetPropertyValues() method is called.
Updating Profile Information
The job of updating profile properties in the SetPropertyValues() is just as straightforward as reading property values. This time, the update stored procedure is used, and every supplied value is translated into a parameter with the same name.
Here’s the complete code:
public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection values)
{
// Prepare the command.
SqlConnection con = new SqlConnection(connectionString); SqlCommand cmd = new SqlCommand(updateProcedure, con); cmd.CommandType = CommandType.StoredProcedure;
//Add the parameters.
//The assumption is that every property maps exactly
//to a single stored procedure parameter name. foreach (SettingsPropertyValue value in values)
{
cmd.Parameters.Add(new SqlParameter(value.Name, value.PropertyValue));
}
//Again, this provider assumes the stored procedure accepts a parameter named
//@UserName.
cmd.Parameters.Add(new SqlParameter("@UserName", (string)context["UserName"]));
// Execute the command. try
{
con.Open();
cmd.ExecuteNonQuery();
}
finally
{
con.Close();
}
}
This completes the code you need for the simple implementation of the FactoredProfileProvider.
836 C H A P T E R 2 4 ■ P R O F I L E S
■Note If you want to mimic the behavior of the SqlProfileProvider, you should also update the database with the last update time whenever the SetPropertyValues() method is called.
Testing the FactoredProfileProvider
To try this example, you need to create, at a bare minimum, a database with a Users table and the two stored procedures. The following example demonstrates an example with a Users table that provides address information (see Figure 24-7)
Figure 24-7. A custom Users table
A straightforward procedure named Users_GetByUserName queries the profile information from the table:
CREATE PROCEDURE Users_GetByUserName @UserName varchar(50)
AS
SELECT * FROM Users WHERE UserName = @UserName
GO
The Users_Update stored procedure is a little more interesting. It begins by checking for the existence of the specified user. If the user doesn’t exist, a record is created with the profile information. If the user does exist, that record is updated. This design meshes with the behavior of the SqlProfileProvider.
■Note Remember, all Profile providers assume the user has already been authenticated. If you’re using the same table to store user authentication information and profile information, an unauthenticated user must have a record in this table. However, this isn’t the case if you use separate tables or Windows authentication.
Here’s the complete code for the Users_Update stored procedure:
CREATE PROCEDURE [Users_Update] (@UserName [varchar](50), @AddressName [varchar](50), @AddressStreet [varchar](50), @AddressCity [varchar](50), @AddressState [varchar](50), @AddressZipCode [varchar](50),
C H A P T E R 2 4 ■ P R O F I L E S |
837 |
@AddressCountry [varchar](50))
AS
DECLARE @Match int
SELECT @Match = COUNT(*) FROM Users WHERE UserName = @UserName
IF (@Match = 0) INSERT INTO Users
(UserName, AddressName, AddressStreet, AddressCity, AddressState, AddressZipCode, AddressCountry)
VALUES
(@UserName, @AddressName, @AddressStreet, @AddressCity, @AddressState, @AddressZipCode, @AddressCountry)
IF (@Match = 1) UPDATE Users SET
[UserName] = @UserName, [AddressName] = @AddressName, [AddressStreet] = @AddressStreet, [AddressCity] = @AddressCity, [AddressState] = @AddressState, [AddressZipCode] = @AddressZipCode, [AddressCountry] = @AddressCountry
WHERE
(UserName = @UserName)
GO
■Note You can download a script to create this table and the corresponding stored procedures with the sample code for this chapter.
To use this table, you simply need to configure the FactoredProfileProvider, identify the stored procedures you’re using, and define all the fields of the Users table that you need to access. Here are the complete web.config configuration details:
<profile defaultProvider="FactoredProfileProvider"> <providers >
<clear />
<add name="FactoredProfileProvider" type="FactoredProfileProvider" connectionStringName="SqlServices" updateUserProcedure="Users_Update" getUserProcedure="Users_GetByUserName"/>
</providers>
<properties>
<add name="AddressName"/> <add name="AddressStreet"/> <add name="AddressCity"/> <add name="AddressState"/> <add name="AddressZipCode"/> <add name="AddressCountry"/>
</properties>
</profile>
