Contents
The profile provider is one of the most prominent features of the ASP.NET 2.0 release; where developers were given an alternative to conventional state management options to store user specific data. Of course, what differentiates the Profile Provider is that user specific information is permanently persisted and stored in a back-end data store, which is in almost every case, for tacitly implied reasons, a SQL Server database.
The most common limitation of the default profile lies not in the performance contrary to initial expectations, but actually in the way data persistence is implemented. Once the data is persisted into the data store, it could only be serialized as one of three formats: String, XML, or Binary; and after that, it would be crammed into one field. Needless to say, writing custom parsing logic with these restrictions is a nightmare.
As with many features in the vast .NET Framework, extensibility is often an option. The system allows for custom Profile Providers; and indeed came to the rescue, a Software Engineer from Microsoft, Hao Kung, who wrote a custom profile provider [^] for both table based and Stored Procedure based approaches. It was quite a popular piece of code that inspired us to delve into the details and under-the-hood workings of the ASP.NET Profile Provider.
Writing a LINQ based custom profile feels like a very natural extrapolation to the profile feature. I am sure this topic has been in the minds of many developers; therefore, I shall try my best to write an article that would hopefully be of some use to other developers out there who would like to leverage the Profile Provider in a .NET 3.5 environment.
This article also features Windows Workflow Foundation; I could not resist implementing the business logic required by the custom Profile Provider using WF. It adds a whole new level of interest, and it also serves as an independent study in using the WF runtime from an ASP.NET Web application.
This section will cover the required design and implementation steps in each domain (website, LINQ to SQL, and WF). I shall demonstrate how to design these building blocks, and then in later sections, I shall fuse them all together.
No doubt, the web.config is sometimes a web developer's best friend. I know it is mine, except for those times when things go awry when Visual Studio 2005 or 2008 dynamically compiles the markup inside the web.config. Going back to the point, here is an excerpt of a recommended configuration:
<system.web>
<profile enabled="true"
automaticSaveEnabled="false"
defaultProvider="CPDemoUserProfileProvider"
inherits="BusinessLogic.BusinessObjects.CPDemoUserProfile">
<providers>
<clear/>
-->
<add name="SqlProvider" type="System.Web.Profile.SqlProfileProvider"
connectionStringName="MyConnectionString"
applicationName="CPDemo"/>
<add name="CPDemoUserProfileProvider"
type="BusinessLogic.Providers.CPDemoUserProfileProvider"
ApplicationName="CPDemo"
ApplicationGUID="DB487110-2809-42F0-BDA0-742C088C75F3"/>
</providers>
</profile>
</system.web>
You may have noticed the absence of the <properties></properties>
tags, but this will be clear once we finish writing our custom Profile object in the next step.
A quick word about the ApplicationGUID
attribute. The Profile Provider database that ships with SQL Server 2005 can accommodate more than one application in a single database. This may not be desirable from a performance standpoint; therefore, to minimize database schema changes, a hard coded value for the application identifier is used. For more information on the Profile Tables schema, please refer to the Microsoft documentation.
For the purposes of this demo, we shall assume a user profile that comprises the usual personal information (name, address, etc.) with some number that corresponds to a successful credit application, along with the approval date. Schematically speaking, the custom Profile object looks something like this:
And now, to the actual code of the user profile. There are several ways to generate the strongly typed version of the user profile:
- Visual Studio automatically generates a strongly typed object based on the profile properties in the web.config
- Third part custom tools can also generate strongly typed Web profile with additional properties
- The old fashioned way: manual coding
It is fairly easy and straightforward to code the custom profile manually if there are no complex data types and PropertyGroups in your profile. For this demo, here is the actual code of the custom Profile object (only a few properties shown here for brevity):
[Serializable]
public class CPDemoUserProfile : ProfileBase
{
[SettingsAllowAnonymous(true)]
[DefaultSettingValue("1/1/0001 12:00:00 AM")]
public virtual System.DateTime LastApprovedDate
{
get
{
return ((System.DateTime)(this.GetPropertyValue("LastApprovedDate")));
}
set
{
this.SetPropertyValue("LastApprovedDate", value);
}
}
[SettingsAllowAnonymous(true)]
[DefaultSettingValue("LightBlue")]
public string Theme
{
get
{
return ((string)(this.GetPropertyValue("Theme")));
}
set
{
this.SetPropertyValue("Theme", value);
}
}
[SettingsAllowAnonymous(true)]
public virtual string State
{
get
{
return ((string)(this.GetPropertyValue("State")));
}
set
{
this.SetPropertyValue("State", value);
}
}
[SettingsAllowAnonymous(true)]
[DefaultSettingValue("1/1/0001 12:00:00 AM")]
public System.DateTime LastProfileUpdateDate
{
get
{
return ((System.DateTime)(this.GetPropertyValue
("LastProfileUpdateDate")));
}
}
[SettingsAllowAnonymous(true)]
public virtual string LastName
{
get
{
return ((string)(this.GetPropertyValue("LastName")));
}
set
{
this.SetPropertyValue("LastName", value);
}
}
}
Usually, when a Profile Provider is configured, Visual Studio tries to compile a strongly typed version of the profile object in question. In a common scenario where the default provider is used, the ProfileCommon
class allows us access to the properties of the user profile. Adding the inherits
directive will force the ProfileCommon
to inherit from our own custom Profile class. This will mainly impact the coding in the ASPX Web page in a positive way such that we can use the Page.Profile
property directly without having to use a cast to the type of our custom User Profile object. To better illustrate this, once the web.config is compiled, the definition of the ProfileCommon
property of the Page
class will be:
public class ProfileCommon : BusinessLogic.BusinessObjects.CPDemoUserProfile {
public virtual ProfileCommon GetProfile(string username) {
return ((ProfileCommon)(ProfileBase.Create(username)));
}
}
Following the Provider Model Design Pattern and Specification [^] adopted and implemented by Microsoft, we can build our own Providers by sub classing from certain base classes. In the case of a custom profile provider, the base class in question is ProfileProvider
. For the purposes of this article, only the methods shown in the following diagram are overridden and implemented.
In the next few sections, we'll fill in the gaps. But for now, this is a skeletal code outline for what we'll be working on:
public class CPDemoUserProfileProvider : ProfileProvider
{
private string _appName = String.Empty;
private Guid _appGuid = Guid.Empty;
private string _providerName = String.Empty;
public override void Initialize(string name, NameValueCollection config)
{
_appGuid = new Guid(config["ApplicationGUID"]);
_appName = config["ApplicationName"];
_providerName = name;
base.Initialize(name, config);
}
public override string Name
{
get { return _providerName; }
}
public override string ApplicationName
{
get { return _appName; }
set { return; }
}
public override SettingsPropertyValueCollection GetPropertyValues
(SettingsContext context, SettingsPropertyCollection collection)
{
throw new NotImplementedException();
}
public override void SetPropertyValues
(SettingsContext context, SettingsPropertyValueCollection collection)
{
throw new NotImplementedException();
}
public override int DeleteInactiveProfiles
(ProfileAuthenticationOption authenticationOption,
DateTime userInactiveSinceDate)
{
throw new NotImplementedException();
}
public int GetTotalNumberofProfiles()
{
throw new NotImplementedException();
}
}
As a quick refresher, the default Microsoft Profile schema can be generated using the aspnet_regsql.exe utility which is usually located in the Windows\Microsoft.NET\Framework\v2.0.50727 directory, by running the following command...
aspnet_regsql.exe -S 'server name' -d 'database name' -A p -E
... which installs all the tables, Views, and Stored Procedures that are bundled with the default profile provider. However, we are interested in only two of these tables: aspnet_Users and aspnet_Application. As you will see, we shall forfeit the need to use any of the provided Stored Procedures. The default table where the user profile is persisted is called aspnet_Profile, but as I mentioned, our goal is to create and use our own table to store the custom profile data.
Of course, you can also run the aspnet_regsql.exe command with no parameters to launch the GUI wizard.
Based on the CPDemoUserProfile
object, we have a pretty good idea on how the table required to store the custom profile data should look like. Being more of a C# guy than a database guy, I chose to design my custom profile from a Top-Down approach, meaning starting the design from within the business logic tier (or business domain). Others may very well choose the Bottom-Up if they are more database oriented developers and architects. At any rate, our custom profile table (also named CPDemoUserProfile) and the two tables we need look like this after importing them into a LINQ to SQL document in Visual Studio 2008.
The extra table called ApprovalNumberHistory was added to demonstrate that extending the Profiles feature need not be limited or restricted. After all, we have ultimate control over all ensuing database operations.
After building the application that contains the LINQ to SQL file, Visual Studio auto-generates the appropriate DataContext
object, with methods and properties mapping to the database schema we chose above. Which brings me to the next step in this architecture. Now, the next step is purely based on personal design and architectural preferences, and whatever other methods of design and implementation you choose to adopt, will not affect the core idea of this article.
I fully realize I am tackling quite a contentious issue. Many would argue the advantages and caveats of using Singleton DataContext
s, and the repercussions of not properly disposing of the DataContext
inside the scope of the calling code. Also, in my design strategy I considered the following factors:
- Improper disposing of the
DataContext
object will yield an ObjectDisposed
exception, if by mistake, the data retrieved by the DataContext
was accessed.
- It is not mandated that a
DataContext
object is to be explicitly disposed of (by calling the Dispose()
method).
- The
DataContext
does not hold any open connections to the database, but rather use currently available connections in the connection pool. Once a DataContext
object has finished its data operations, the connection is returned to the pool. Check out this blog by Scott Guthrie [^] for more in depth discussions.
Added to the aforementioned factors, I personally prefer to have multiple Singletons, each responsible for an atomic unit of logic, as opposed to using one gargantuan object that handles multiple responsibilities. To give a concrete example; in my real life project I currently am working on (which inspired me to write this article), the back end not only contains a Custom Profile schema, but also, many others related to User Management, Finance, and other domain specific entities. As a more manageable solution, I decided to have several DataContext
Singletons, each dedicated to one independent domain unit in the database. This explains why the DataContext
for this article includes only tables and entities related to the custom profile feature. The resulting DataContext
will be exclusively responsible for Profile related operations.
After that prelude, I can present the ProfileDataContext
that is based on the LINQ to SQL schema presented earlier.
Now that I have established the premise for the multiple DataContext
Singletons design pattern, I can introduce a small utility class that manages the creation of these Singletons. The DataContextManager<T>
class has the following skeleton:
public static class DataContextManager<T> where T : DataContext, new()
{
private static T _context = null;
static DataContextManager()
{
lock (typeof(DataContextManager<T>))
{
if (null == _context)
{
_context = new T();
}
}
}
public static T GetInstance()
{
return (null == _context) ? new T() : _context;
}
public static void UpdateEntity<K>(K entity, Action<K> updateAction)
where K : class
{
T instance = GetInstance();
instance.GetTable<K>().Attach(entity, true);
updateAction(entity);
instance.SubmitChanges();
}
public static void DeleteEntity<K>(K entity) where K : class
{
T instance = GetInstance();
Table<K> table = instance.GetTable<K>();
table.Attach(entity);
table.DeleteOnSubmit(entity);
instance.SubmitChanges();
}
}
And, here is an example to demonstrate how to use the DataContextManager<T>
:
ProfileDataContext db = DataContextManager<ProfileDataContext>.GetInstance();
IQueryable<aspnet_User> users = db.CPDemoUserProfiles.Select
(p => p.aspnet_User).AsQueryable();
One more thing I appreciate about having feature-specific DataContext
objects, is that by using the partial class feature, we can extend the auto-generated DataContext
classes and add our own custom loading logic and other property validation logic. I present here a small demonstration of how we can optimize the SQL statements emitted by the LINQ engine by changing the loading options of the DataContext
:
partial class ProfileDataContext
{
private StreamWriter _log;
partial void OnCreated()
{
DataLoadOptions options = new DataLoadOptions();
options.LoadWith<CPDemoUserProfile>(p => p.aspnet_User);
this.LoadOptions = options;
_log = new StreamWriter(@"C:\Logs\ProfileDataContext.txt",
false, System.Text.Encoding.ASCII);
_log.AutoFlush = true;
this.Log = _log;
}
protected override void Dispose(bool disposing)
{
_log.Close();
_log.Dispose();
base.Dispose(disposing);
}
}
partial class CPDemoUserProfile
{
partial void OnLastApprovedDateChanged()
{
DateTime? value = this._LastApprovedDate;
if (value > DateTime.MaxValue || value < DateTime.MinValue)
this._LastApprovedDate = default(DateTime?);
}
}
I will not attempt, in this section, to delve into the implementation details of the Workflow Foundation and how to design and execute workflows. I will, however, mention a couple of issues that rise from the fact that the hosting environment is a Web application. The two main issues are:
- Caching the Workflow Runtime
- Forcing a synchronous workflow execution
The first issue could be solved using the Application
object of the HttpContext
class to globally cache the Runtime. The Runtime could be started and shutdown using the Application-level events: Application_Start
and Application_End
, respectively. These events, of course, are exposed in the Global.asax file.
As for the second problem. We have to talk a little about the thread management in the Workflow Runtime engine. That task (referring to workflow execution) is the responsibility of the scheduler service, one of the core services of the Workflow Runtime. The default service, named DefaultWorkflowSchedulerService
, executes the workflows asynchronously using a thread pool. This is, of course, problematic in the ASP.NET environment, given the stateless state of Web requests. So, in order to ensure a synchronous execution, we have to load the ManualWorkflowSchedulerService
into the Runtime. This service uses the host application's thread to execute the workflow, which circumvents the asynchronous threading issue.
Generally, it is a good idea to have a utility class that handles starting and terminating the runtime as well as executing workflows. I will not list my own implementation lest I drown the article with code listings, unless, of course, such code is requested by the readers. So, the reminder of the article assumes the presence of one such utility class, having a similar structure to:
The following two sections illustrate how to implement the most two important methods of our custom provider (viz. GetPropertyValues
and SetPropertyValues
) as workflows. Other custom provider method implementations are not provided in detail, but are left to the capable minds of the reader to improve upon with their own implementations.
And, here is the C# code translation of that workflow:
public sealed partial class GetProfilePropertiesWorkflow : SequentialWorkflowActivity
{
public GetProfilePropertiesWorkflow()
{
InitializeComponent();
}
public SettingsPropertyCollection SettingsCollection { get; set; }
public Guid ApplicationGuid { get; set; }
public string UserName { get; set; }
public CPDemoUserProfile UserProfile { get; set; }
public SettingsPropertyValueCollection ProfileSettings { get; set; }
private void InitializePropertyCollection_ExecuteCode(object sender, EventArgs e)
{
ProfileSettings = new SettingsPropertyValueCollection();
IEnumerable<SettingsProperty> _collection =
SettingsCollection.Cast<SettingsProperty>();
foreach (SettingsProperty sp in _collection)
{
SettingsPropertyValue value = new SettingsPropertyValue(sp);
ProfileSettings.Add(value);
}
}
private void GetUserProfile_ExecuteCode(object sender, EventArgs e)
{
ProfileDataContext db = DataContextManager<ProfileDataContext>.GetInstance();
UserProfile = (from u in db.aspnet_Users
where u.LoweredUserName ==
UserName.ToLower() && u.ApplicationId == ApplicationGuid
join p in db.CPDemoUserProfiles on u.UserId equals p.UserId
select p).SingleOrDefault<CPDemoUserProfile>();
}
private void PopulatePropertyValues_ExecuteCode(object sender, EventArgs e)
{
if (null != UserProfile)
{
IEnumerable<SettingsProperty> _collection =
SettingsCollection.Cast<SettingsProperty>();
Type type = UserProfile.GetType();
foreach (SettingsProperty sp in _collection)
{
SettingsPropertyValue value = ProfileSettings[sp.Name];
if (null != value)
{
if (value.UsingDefaultValue)
value.PropertyValue = Convert.ChangeType(
value.Property.DefaultValue, value.Property.PropertyType);
PropertyInfo pi = type.GetProperty(sp.Name);
object pv = pi.GetValue(UserProfile, null);
if (null != pv && !(pv is DBNull))
value.PropertyValue = pv;
value.IsDirty = false;
value.Deserialized = true;
}
}
}
}
}
And, here is the C# code translation of that workflow. A little more complicated than GetPropertyValues
!
public sealed partial class SetProfilePropertiesWorkflow : SequentialWorkflowActivity
{
public SetProfilePropertiesWorkflow()
{
InitializeComponent();
}
public Guid ApplicationGuid { get; set; }
public string UserName { get; set; }
public bool isUserAuthenticated { get; set; }
public aspnet_User UserEntity { get; set; }
public SettingsPropertyValueCollection ProfileSettings { get; set; }
private void GetUser_ExecuteCode(object sender, EventArgs e)
{
ProfileDataContext db = DataContextManager<ProfileDataContext>.GetInstance();
UserEntity = (from u in db.aspnet_Users
where u.ApplicationId == ApplicationGuid &&
u.LoweredUserName == UserName.ToLower()
select u).SingleOrDefault<aspnet_User>();
}
private void UserFoundCondition(object sender, ConditionalEventArgs e)
{
e.Result = (UserEntity != null) ? true : false;
}
private static class EntityConverter<T>
{
public static void CopyValues(SettingsPropertyValueCollection source, T target)
{
IEnumerable<SettingsPropertyValue> _source =
source.Cast<SettingsPropertyValue>();
foreach (SettingsPropertyValue sv in _source)
{
PropertyInfo pi = target.GetType().GetProperty(sv.Name);
if (null != pi && sv.IsDirty)
{
if (sv.Deserialized && null == sv.PropertyValue)
pi.SetValue(target, DBNull.Value, null);
else
pi.SetValue(target, sv.PropertyValue, null);
}
}
}
}
private void UpdateUserProfile_ExecuteCode(object sender, EventArgs e)
{
CPDemoUserProfile profile = UserEntity.CPDemoUserProfile;
bool isOrphanUser = false;
if (null == profile)
{
profile = new CPDemoUserProfile();
isOrphanUser = true;
}
EntityConverter<CPDemoUserProfile>.CopyValues(ProfileSettings, profile);
UserEntity.LastActivityDate = DateTime.Now;
profile.LastProfileUpdateDate = DateTime.Now;
if (isOrphanUser)
UserEntity.CPDemoUserProfile = profile;
DataContextManager<ProfileDataContext>.GetInstance().SubmitChanges();
}
private void CreateNewUserProfile_ExecuteCode(object sender, EventArgs e)
{
Guid guid = Guid.NewGuid();
aspnet_User newUser = new aspnet_User();
newUser.IsAnonymous = !isUserAuthenticated;
newUser.UserId = guid;
newUser.UserName = UserName;
newUser.LoweredUserName = UserName.ToLower();
newUser.LastActivityDate = DateTime.Now;
newUser.ApplicationId = ApplicationGuid;
CPDemoUserProfile profile = new CPDemoUserProfile();
EntityConverter<CPDemoUserProfile>.CopyValues(ProfileSettings, profile);
profile.LastProfileUpdateDate = DateTime.Now;
newUser.CPDemoUserProfile = profile;
ProfileDataContext db = DataContextManager<ProfileDataContext>.GetInstance();
db.aspnet_Users.InsertOnSubmit(newUser);
db.SubmitChanges();
}
}
Now, to the culmination of all those technology-spanning efforts, the final custom profile provider implementation. As I mentioned earlier, only the most significant methods are implemented, and the rest are left to your capable coding hands!
public class CPDemoUserProfileProvider : ProfileProvider
{
private string _appName = String.Empty;
private Guid _appGuid = Guid.Empty;
private string _providerName = String.Empty;
public override void Initialize(string name, NameValueCollection config)
{
_appGuid = new Guid(config["ApplicationGUID"]);
_appName = config["ApplicationName"];
_providerName = name;
base.Initialize(name, config);
}
public override string Name
{
get { return _providerName; }
}
public override string ApplicationName
{
get { return _appName; }
set { return; }
}
public override SettingsPropertyValueCollection GetPropertyValues
(SettingsContext context, SettingsPropertyCollection collection)
{
string userName = (string)context["UserName"];
Dictionary<string, object> properties = new Dictionary<string, object>();
properties.Add("SettingsCollection", collection);
properties.Add("UserName", userName);
properties.Add("ApplicationGuid", _appGuid);
properties.Add("ProfileSettings", null);
Core.WorkflowManager.ExecuteWorkflow(typeof(
Workflows.UserProfileWorkflows.GetProfilePropertiesWorkflow),
properties);
return properties["ProfileSettings"] as
SettingsPropertyValueCollection;
}
public override void SetPropertyValues(SettingsContext context,
SettingsPropertyValueCollection collection)
{
string userName = (string)context["UserName"];
bool userIsAuthenticated = (bool)context["IsAuthenticated"];
Dictionary<string, object> properties =
new Dictionary<string, object>();
properties.Add("ProfileSettings", collection);
properties.Add("UserName", userName);
properties.Add("isUserAuthenticated", userIsAuthenticated);
properties.Add("ApplicationGuid", _appGuid);
Core.WorkflowManager.ExecuteWorkflow(typeof(
Workflows.UserProfileWorkflows.SetProfilePropertiesWorkflow),
properties);
}
public override int DeleteInactiveProfiles(ProfileAuthenticationOption
authenticationOption, DateTime userInactiveSinceDate)
{
int ret = -1;
using (TransactionScope ts = new TransactionScope())
{
ProfileDataContext db =
DataContextManager<ProfileDataContext>.GetInstance();
IEnumerable<CPDemoUserProfile> profilestoDelete =
from p in db.CPDemoUserProfiles
where p.aspnet_User.LastActivityDate
<= userInactiveSinceDate
&& (authenticationOption ==
ProfileAuthenticationOption.All
|| (authenticationOption ==
ProfileAuthenticationOption.Anonymous &&
p.aspnet_User.IsAnonymous)
|| (authenticationOption ==
ProfileAuthenticationOption.Authenticated
&& !p.aspnet_User.IsAnonymous))
select p;
ret = profilestoDelete.Count();
db.CPDemoUserProfiles.DeleteAllOnSubmit(profilestoDelete);
db.SubmitChanges();
ts.Complete();
}
return ret;
}
public int GetTotalNumberofProfiles()
{
ProfileDataContext db = DataContextManager<ProfileDataContext>.GetInstance();
IEnumerable<CPDemoUserProfile> profiles = (from p in db.CPDemoUserProfiles
select p).DefaultIfEmpty();
return profiles.Count();
}
}
I hope the article was of some benefit and inspiration to others. This article came out of my current work on a Web application that implements the ASP.NET Profile Provider. I thought it would be interesting to architect the custom provider in such a way that it encompasses both LINQ and the Workflow Foundation. Consequently, I decided to make this design the topic of my very first CodeProject contribution.
Understandably, I could not copy my current production code and use it in this article, so the code presented here is intended as a workable code skeleton that can be adopted and enhanced. It was stripped down to a bare minimum. Personally, in production code, I use compiled queries for better performance, asynchronous Page Tasks, and robust exception handling mechanisms.
Happy coding!
- "Pro WF" from Apress, by Bruce Bukovics
- "Pro LINQ Object Relational Mapping with C# 2008" from Apress, by Vijay P Mehta
- "Programming WCF Services" from O'Reilly, by Juval Lowy
- "Beautiful Code" from O'Reilly, edited by Oram & Wilson
I also regularly check out these Blogs: