|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Contents
OverviewThe 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. The FoundationsThis 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. Configuring the Web ApplicationNo 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/>
<!--
The SqlProvider is added here to demonstrate that
it is possible to
declare and use multiple profile Providers at once.
-->
<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 A quick word about the Building the Custom ProfileFor 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:
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 public class ProfileCommon : BusinessLogic.BusinessObjects.CPDemoUserProfile {
public virtual ProfileCommon GetProfile(string username) {
return ((ProfileCommon)(ProfileBase.Create(username)));
}
}
Prototyping the ProfileProvider ClassFollowing 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
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();
}
/*
* Custom ProfileManger methods
*/
public int GetTotalNumberofProfiles()
{
throw new NotImplementedException();
}
}
LINQ to SQL DataContextAs 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
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 as a SingletonI fully realize I am tackling quite a contentious issue. Many would argue the advantages and caveats of using Singleton
Feature-Specific Singleton PatternAdded 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 After that prelude, I can present the
Now that I have established the premise for the multiple 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();
//As a precaution attach entity to a DataContext before update.
//Context might be lost when passing through application boundaries.
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 ProfileDataContext db = DataContextManager<ProfileDataContext>.GetInstance();
IQueryable<aspnet_User> users = db.CPDemoUserProfiles.Select
(p => p.aspnet_User).AsQueryable();
Extending the DataContext Partial ClassOne more thing I appreciate about having feature-specific partial class ProfileDataContext
{
private StreamWriter _log;
partial void OnCreated()
{
//LoadOptions
DataLoadOptions options = new DataLoadOptions();
options.LoadWith<CPDemoUserProfile>(p => p.aspnet_User);
this.LoadOptions = options;
/*
* It is always a good idea to keep an eye on what SQL statements
* are emitted by LINQ engine
* this could be a good practice during development phase
* so that appropriate database optimization
* measures are taken (indexes, query optimization, etc...).
*/
_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);
}
}
//Example of some validation rules on property values
partial class CPDemoUserProfile
{
partial void OnLastApprovedDateChanged()
{
DateTime? value = this._LastApprovedDate;
if (value > DateTime.MaxValue || value < DateTime.MinValue)
this._LastApprovedDate = default(DateTime?);
}
}
Business Logic Using the Workflow FoundationHosting the Workflow Runtime in IISI 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:
The first issue could be solved using the 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 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 Workflow
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)
{
//Very important to populate the SettingsPropertyValueCollection with
//property names from the custom user profile
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;
}
}
}
}
}
SetPropertyValues Workflow
And, here is the C# code translation of that workflow. A little more complicated than 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)//set only properties that changed.
{
//incase value could not be deserialized properly
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;
//incase we have an orphan user record with no profile.
//Might happen if we delete
//a profile without deleting the corresponding user record.
if (null == profile)
{
profile = new CPDemoUserProfile();
isOrphanUser = true;
}
//Note:
//Using reflection to populate values.
//This way we ensure reusability of this logic
//across other projects.
EntityConverter<CPDemoUserProfile>.CopyValues(ProfileSettings, profile);
//Update user table with latest activity date
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();
}
}
Putting It All TogetherNow, 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;
}
/*
* Custom ProfileManger methods
*/
public int GetTotalNumberofProfiles()
{
ProfileDataContext db = DataContextManager<ProfileDataContext>.GetInstance();
IEnumerable<CPDemoUserProfile> profiles = (from p in db.CPDemoUserProfiles
select p).DefaultIfEmpty();
return profiles.Count();
}
}
ConclusionI 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! Recommended Reading
I also regularly check out these Blogs:
History
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||