Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

LINQ and WF Based Custom Profile Provider for ASP.NET 3.5

0.00/5 (No votes)
8 Dec 2008 1  
A new implementation of the Custom Profile Provider for ASP.NET 3.5, using LINQ, Workflow Foundation, and the Responsibility-Centric-Singleton DataContexts pattern.

Contents

Overview

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.

The Foundations

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.

Configuring the Web Application

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/>
        <!--
            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 <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.

Building the Custom Profile

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:

CPDemoUserProfile.jpg

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)));
    }
}

Prototyping the ProfileProvider Class

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.

CPDemoUserProfileProvider.jpg

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 DataContext

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.

ProfileDBSchema.jpg

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.

DataContext as a Singleton

I fully realize I am tackling quite a contentious issue. Many would argue the advantages and caveats of using Singleton DataContexts, 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.

Feature-Specific Singleton Pattern

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.

ProfileDataContext.jpg

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();
        //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 DataContextManager<T>:

ProfileDataContext db = DataContextManager<ProfileDataContext>.GetInstance();
IQueryable<aspnet_User> users = db.CPDemoUserProfiles.Select
                (p => p.aspnet_User).AsQueryable();

Extending the DataContext Partial Class

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()
    {
        //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 Foundation

Hosting the Workflow Runtime in IIS

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:

  1. Caching the Workflow Runtime
  2. 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:

WorkflowManager.jpg

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.

GetPropertyValues Workflow

GetProfilePropertiesWorkflow.jpg

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

SetProfilePropertiesWorkflow.jpg

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)//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 Together

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;
    }

    /*
     * 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();
    }
}

Conclusion

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!

Recommended Reading

  • "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:

History

  • 27 Nov 2008 - First version submitted
  • 07 Dec 2008 - Added a working sample with source code, minor editorial changes and updated the Prototyping the ProfileProvider Class code listing

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here